Repository: iterative/gto (language: python)

Issue:
Register pre-gto model versions
I may start using gto after already having a bunch of artifacts from previous commits that I want to register. There's no way that I can tell to register these older artifact versions since I haven't added them to `artifacts.yaml` in those commits (`artifacts.yaml` may not even exist).

More generally, should the metadata be read from the registered commit or from the current/latest commit?


Discussion / hints:

Fix patch:
diff --git a/gto/api.py b/gto/api.py
index 9ba2471c..b4500bad 100644
--- a/gto/api.py
+++ b/gto/api.py
@@ -5,7 +5,13 @@
 from git import Repo
 
 from gto.constants import NAME, STAGE, VERSION
-from gto.index import FileIndexManager, RepoIndexManager, init_index_manager
+from gto.ext import EnrichmentInfo
+from gto.index import (
+    EnrichmentManager,
+    FileIndexManager,
+    RepoIndexManager,
+    init_index_manager,
+)
 from gto.registry import GitRegistry
 from gto.tag import parse_name
 
@@ -21,7 +27,7 @@ def get_index(repo: Union[str, Repo], file=False):
 
 def _get_state(repo: Union[str, Repo]):
     """Show current registry state"""
-    return GitRegistry.from_repo(repo).state
+    return GitRegistry.from_repo(repo).get_state()
 
 
 def get_stages(repo: Union[str, Repo], in_use: bool = False):
@@ -107,7 +113,8 @@ def find_promotion(repo: Union[str, Repo], name: str, stage: str):
 def check_ref(repo: Union[str, Repo], ref: str):
     """Find out what have been registered/promoted in the provided ref"""
     reg = GitRegistry.from_repo(repo)
-    ref = ref.removeprefix("refs/tags/")
+    if ref.startswith("refs/tags/"):
+        ref = ref[len("refs/tags/") :]
     if ref.startswith("refs/heads/"):
         ref = reg.repo.commit(ref).hexsha
     result = reg.check_ref(ref)
@@ -117,13 +124,39 @@ def check_ref(repo: Union[str, Repo], ref: str):
     }
 
 
-def show(repo: Union[str, Repo], object: str = "registry", table: bool = False):
+def show(
+    repo: Union[str, Repo],
+    object: str = "registry",
+    discover: bool = False,
+    table: bool = False,
+    all_branches=False,
+    all_commits=False,
+):
     if object == "registry":
-        return _show_registry(repo, table=table)
-    return _show_versions(repo, name=object, table=table)
+        return _show_registry(
+            repo,
+            discover=discover,
+            all_branches=all_branches,
+            all_commits=all_commits,
+            table=table,
+        )
+    return _show_versions(
+        repo,
+        name=object,
+        discover=discover,
+        all_branches=all_branches,
+        all_commits=all_commits,
+        table=table,
+    )
 
 
-def _show_registry(repo: Union[str, Repo], table: bool = False):
+def _show_registry(
+    repo: Union[str, Repo],
+    discover: bool = False,
+    all_branches=False,
+    all_commits=False,
+    table: bool = False,
+):
     """Show current registry state"""
 
     reg = GitRegistry.from_repo(repo)
@@ -136,7 +169,11 @@ def _show_registry(repo: Union[str, Repo], table: bool = False):
                 for name in stages
             },
         }
-        for o in reg.state.artifacts.values()
+        for o in reg.get_state(
+            discover=discover, all_branches=all_branches, all_commits=all_commits
+        )
+        .get_artifacts()
+        .values()
     }
     if not table:
         return models_state
@@ -150,117 +187,123 @@ def _show_registry(repo: Union[str, Repo], table: bool = False):
 
 
 def _show_versions(
-    repo: Union[str, Repo], name: str, raw: bool = False, table: bool = False
+    repo: Union[str, Repo],
+    name: str,
+    raw: bool = False,
+    discover: bool = False,
+    all_branches=False,
+    all_commits=False,
+    table: bool = False,
 ):
     """List versions of artifact"""
     reg = GitRegistry.from_repo(repo)
     if raw:
-        return reg.state.find_artifact(name).versions
-    versions = [v.dict_status() for v in reg.state.find_artifact(name).versions]
+        return reg.find_artifact(name).versions
+    versions = [
+        v.dict_status()
+        for v in reg.find_artifact(
+            name, discover=discover, all_branches=all_branches, all_commits=all_commits
+        ).get_versions(discover=discover)
+    ]
     if not table:
         return versions
 
     first_keys = ["artifact", "name", "stage"]
     versions_ = []
     for v in versions:
-        v["artifact"] = v["artifact"]["name"]
-        v["stage"] = v["stage"]["stage"]
+        if v["stage"]:
+            v["stage"] = v["stage"]["stage"]
         v = OrderedDict(
             [(key, v[key]) for key in first_keys]
             + [(key, v[key]) for key in v if key not in first_keys]
         )
         v["commit_hexsha"] = v["commit_hexsha"][:7]
+        v["enrichments"] = [e["source"] for e in v["enrichments"]]
         versions_.append(v)
     return versions_, "keys"
 
 
-def audit_registration(
+def _is_ascending(sort):
+    return sort in {"asc", "Asc", "ascending", "Ascending"}
+
+
+def describe(
+    repo: Union[str, Repo], name: str, rev: str = None
+) -> List[EnrichmentInfo]:
+    """Find enrichments for the artifact"""
+    return EnrichmentManager.from_repo(repo).describe(name=name, rev=rev)
+
+
+def history(
     repo: Union[str, Repo],
     artifact: str = None,
+    discover: bool = False,
+    # action: str = None,
+    all_branches=False,
+    all_commits=False,
     sort: str = "desc",
     table: bool = False,
 ):
-    """Audit registry state"""
+    # TODO: rework this.
+    # 1. commits should be gathered only --discover is supplied
+    # 2. we shouldn't use audit_something functions
+    # 3. commits should be got from EnrichmentManager, probably
+
     reg = GitRegistry.from_repo(repo)
+    artifacts = reg.get_artifacts(
+        discover=discover, all_branches=all_branches, all_commits=all_commits
+    )
+
+    commits = [
+        OrderedDict(
+            timestamp=datetime.fromtimestamp(
+                reg.repo.commit(v.commit_hexsha).committed_date
+            ),
+            name=o.name,
+            event="commit [discovered]" if v.discovered else "commit",
+            commit=v.commit_hexsha[:7],
+            author=reg.repo.commit(v.commit_hexsha).author.name,
+        )
+        for o in artifacts.values()
+        for v in o.get_versions(discover=discover)
+    ]
 
-    audit_trail = [
+    registration = [
         OrderedDict(
             timestamp=v.creation_date,
             name=o.name,
+            event="registration",
             version=v.name,
             commit=v.commit_hexsha[:7],
             author=v.author,
+            # enrichments=[e.source for e in v.enrichments],
         )
-        for o in reg.state.artifacts.values()
-        for v in o.versions
+        for o in artifacts.values()
+        for v in o.get_versions()
     ]
-    if artifact:
-        audit_trail = [event for event in audit_trail if event["name"] == artifact]
-    audit_trail.sort(key=lambda x: x["timestamp"])
-    if _is_ascending(sort):
-        audit_trail.reverse()
-    if not table:
-        return audit_trail
-    return audit_trail, "keys"
-
 
-def _is_ascending(sort):
-    return sort in {"asc", "Asc", "ascending", "Ascending"}
-
-
-def audit_promotion(
-    repo: Union[str, Repo],
-    artifact: str = None,
-    sort: str = "desc",
-    table: bool = False,
-):
-    """Audit registry state"""
-    reg = GitRegistry.from_repo(repo)
-    audit_trail = [
+    promotion = [
         OrderedDict(
             timestamp=l.creation_date,
             name=o.name,
+            event="promotion",
             version=l.version,
             stage=l.stage,
             commit=l.commit_hexsha[:7],
             author=l.author,
         )
-        for o in reg.state.artifacts.values()
+        for o in artifacts.values()
         for l in o.stages
     ]
-    if artifact:
-        audit_trail = [event for event in audit_trail if event["name"] == artifact]
-    audit_trail.sort(key=lambda x: x["timestamp"])
-    if _is_ascending(sort):
-        audit_trail.reverse()
-    if not table:
-        return audit_trail
-    return audit_trail, "keys"
-
 
-def history(repo: str, artifact: str = None, sort: str = "desc", table: bool = False):
-    def add_event(event_list, event_name):
-        return [{**event, "event": event_name} for event in event_list]
-
-    reg = GitRegistry.from_repo(repo)
-    commits = [
-        dict(
-            timestamp=datetime.fromtimestamp(reg.repo.commit(commit).committed_date),
-            name=name_,
-            event="commit",
-            commit=commit[:7],
-            author=reg.repo.commit(commit).author.name,
-        )
-        for name_, commit_list in reg.index.artifact_centric_representation().items()
-        for commit in commit_list
-    ]
-    registration = audit_registration(repo, table=False)
-    promotion = audit_promotion(repo, table=False)
-    events_order = {"commit": 0, "registration": 1, "promotion": 2}
+    events_order = {
+        "commit": 0,
+        "commit [discovered]": 1,
+        "registration": 2,
+        "promotion": 3,
+    }
     events = sorted(
-        add_event(commits, "commit")
-        + add_event(registration, "registration")
-        + add_event(promotion, "promotion"),
+        commits + registration + promotion,
         key=lambda x: (x["timestamp"], events_order[x["event"]]),
     )
     if _is_ascending(sort):
@@ -275,6 +318,7 @@ def add_event(event_list, event_name):
         "event",
         VERSION,
         STAGE,
+        # "enrichments",
         "commit",
         "author",
     ]
diff --git a/gto/base.py b/gto/base.py
index 8ebaa325..e5034922 100644
--- a/gto/base.py
+++ b/gto/base.py
@@ -6,14 +6,14 @@
 
 from gto.config import RegistryConfig
 from gto.constants import Action
-from gto.index import Artifact, ArtifactCommits
-from gto.versions import NumberedVersion, SemVer
+from gto.ext import Enrichment
+from gto.versions import SemVer
 
 from .exceptions import ArtifactNotFound, ManyVersions, VersionRequired
 
 
 class BasePromotion(BaseModel):
-    artifact: Artifact
+    artifact: str
     version: str
     stage: str
     creation_date: datetime
@@ -22,20 +22,24 @@ class BasePromotion(BaseModel):
 
 
 class BaseVersion(BaseModel):
-    artifact: Artifact
+    artifact: str
     name: str
     creation_date: datetime
     author: str
     commit_hexsha: str
+    discovered: bool = False
     promotions: List[BasePromotion] = []
+    enrichments: List[Enrichment] = []
+
+    @property
+    def is_registered(self):
+        """Tells if this is an explicitly registered version"""
+        return SemVer.is_valid(self.name)
 
     @property
     def version(self):
         # TODO: this should be read from config, how to pass it down here?
-        try:
-            return NumberedVersion(self.name)
-        except:  # pylint: disable=bare-except
-            return SemVer(self.name)
+        return SemVer(self.name)
 
     @property
     def stage(self):
@@ -50,7 +54,6 @@ def dict_status(self):
 
 class BaseArtifact(BaseModel):
     name: str
-    commits: ArtifactCommits
     versions: List[BaseVersion]
 
     @property
@@ -68,7 +71,7 @@ def __repr__(self) -> str:
 
     def get_latest_version(self) -> Optional[BaseVersion]:
         versions = sorted(
-            self.versions,
+            (v for v in self.versions if not v.discovered),
             key=lambda x: x.creation_date,
         )
         if versions:
@@ -78,35 +81,97 @@ def get_latest_version(self) -> Optional[BaseVersion]:
     @property
     def promoted(self) -> Dict[str, BasePromotion]:
         stages: Dict[str, BasePromotion] = {}
-        for version in sorted(self.versions, key=lambda x: x.version, reverse=True):
+        # semver
+        versions = sorted(
+            (v for v in self.versions if v.is_registered),
+            key=lambda x: x.version,
+            reverse=True,
+        )
+        # TODO: regular commits - these should be sorted by stage tag creation maybe?
+        # E.g. when you created "rf#prod" tag, then you created a version.
+        # It's probably should be a flag on CLI that will tell how to sort promoted for return.
+        versions.extend(
+            sorted(
+                (v for v in self.versions if not v.is_registered),
+                key=lambda x: x.creation_date,
+                reverse=True,
+            )
+        )
+        for version in versions:
             promotion = version.stage
             if promotion:
                 stages[promotion.stage] = stages.get(promotion.stage) or promotion
         return stages
 
+    def add_version(self, version: BaseVersion):
+        self.versions.append(version)
+
     def add_promotion(self, promotion: BasePromotion):
         self.find_version(name=promotion.version).promotions.append(  # type: ignore
             promotion
         )
 
+    def update_enrichments(self, version, enrichments):
+        self.find_version(
+            name=version, include_discovered=True
+        ).enrichments = enrichments
+
+    @property
+    def discovered(self):
+        return any(not v.discovered for v in self.versions)
+
+    # @overload
+    # def find_version(
+    #     self,
+    #     name: str = None,
+    #     commit_hexsha: str = None,
+    #     raise_if_not_found: Literal[True] = ...,
+    #     allow_multiple: Literal[False] = ...,
+    # ) -> BaseVersion:
+    #     ...
+
+    # @overload
+    # def find_version(
+    #     self,
+    #     name: str = None,
+    #     commit_hexsha: str = None,
+    #     raise_if_not_found: Literal[False] = ...,
+    #     allow_multiple: Literal[False] = ...,
+    # ) -> Optional[BaseVersion]:
+    #     ...
+
+    # @overload
+    # def find_version(
+    #     self,
+    #     name: str = None,
+    #     commit_hexsha: str = None,
+    #     raise_if_not_found: Literal[False] = ...,
+    #     allow_multiple: Literal[True] = ...,
+    # ) -> List[BaseVersion]:
+    #     ...
+
+    def get_versions(self, discover=False):
+        return [v for v in self.versions if discover or not v.discovered]
+
     def find_version(
         self,
         name: str = None,
         commit_hexsha: str = None,
-        raise_if_not_found=False,
+        raise_if_not_found: bool = False,
         allow_multiple=False,
+        include_discovered=False,
     ) -> Union[None, BaseVersion, List[BaseVersion]]:
         versions = [
             v
             for v in self.versions
             if (v.name == name if name else True)
             and (v.commit_hexsha == commit_hexsha if commit_hexsha else True)
+            and (True if include_discovered else not v.discovered)
         ]
+
         if allow_multiple:
             return versions
         if raise_if_not_found and not versions:
-            for v in self.versions:
-                print(v)
             raise VersionRequired(name=self.name)
         if len(versions) > 1:
             raise ManyVersions(
@@ -130,16 +195,29 @@ def find_version_at_commit(
 
 
 class BaseRegistryState(BaseModel):
-    artifacts: Dict[str, BaseArtifact]
+    artifacts: Dict[str, BaseArtifact] = {}
 
     class Config:
         arbitrary_types_allowed = True
 
-    def find_artifact(self, name):
-        art = self.artifacts.get(name)
-        if not art:
-            raise ArtifactNotFound(name)
-        return art
+    def add_artifact(self, name):
+        self.artifacts[name] = BaseArtifact(name=name, versions=[])
+
+    def update_artifact(self, artifact: BaseArtifact):
+        self.artifacts[artifact.name] = artifact
+
+    def get_artifacts(self):
+        return self.artifacts
+
+    def find_artifact(self, name: str, create_new=False):
+        if not name:
+            raise ValueError("Artifact name is required")
+        if name not in self.artifacts:
+            if create_new:
+                self.artifacts[name] = BaseArtifact(name=name, versions=[])
+            else:
+                raise ArtifactNotFound(name)
+        return self.artifacts.get(name)
 
     @property
     def unique_stages(self):
@@ -162,7 +240,7 @@ def which(self, name, stage, raise_if_not_found=True):
         return None
 
     def sort(self):
-        for name in self.artifacts:
+        for name in self.artifacts:  # pylint: disable=consider-using-dict-items
             self.artifacts[name].versions.sort(key=lambda x: (x.creation_date, x.name))
             self.artifacts[name].stages.sort(
                 key=lambda x: (x.creation_date, x.version, x.stage)
diff --git a/gto/branch.py b/gto/branch.py
deleted file mode 100644
index 27d5f367..00000000
--- a/gto/branch.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# pylint: disable=no-self-argument, no-else-return, unused-argument, no-self-use, unused-import
-import logging
-from datetime import datetime
-from typing import Dict, FrozenSet, List
-
-import git
-
-from .base import BaseManager, BasePromotion, BaseRegistryState
-from .constants import Action
-
-
-def traverse_commit_check(commit: git.Commit, desired: git.Commit):
-    if commit == desired:
-        return True
-    return any(traverse_commit_check(c, desired) for c in commit.parents)
-
-
-def find_branches(repo: git.Repo, desired: str) -> List[git.Head]:
-    return [
-        branch
-        for branch in repo.heads
-        if traverse_commit_check(branch.commit, repo.commit(desired))
-    ]
-
-
-class BranchStageManager(BaseManager):
-    actions: FrozenSet[Action] = frozenset((Action.PROMOTE,))
-
-    def update_state(self, state: BaseRegistryState) -> BaseRegistryState:
-        if self.config.VERSION_REQUIRED_FOR_STAGE:
-            # we assume that the model is promoted the same moment it is registered
-            for name in state.artifacts:
-                for version in state.artifacts[name].versions:
-                    # TODO: For each branch that has this commit in history
-                    # we assume the model was promoted to corresponding stage
-                    # we should see are there any options to do this differently
-                    for branch in find_branches(self.repo, version.commit_hexsha):
-                        # figure out stage from branch name
-                        stage = self.config.branch_to_stage(branch.name)
-                        if stage is None:
-                            continue
-                        state.artifacts[name].add_promotion(
-                            BasePromotion(
-                                artifact=version.artifact,
-                                version=version.name,
-                                stage=stage,
-                                creation_date=version.creation_date,
-                                author=version.author,
-                                commit_hexsha=version.commit_hexsha,
-                            )
-                        )
-        else:
-            # we assume each commit in a branch is a promotion to branch stage
-            # if artifact was indexed
-            for name, artifact in state.artifacts.items():
-                for hexsha, index_artifact in artifact.commits.items():
-                    commit = self.repo.commit(hexsha)
-                    version = state.artifacts[name].find_version(commit_hexsha=hexsha)  # type: ignore
-                    version = version.name if version else hexsha  # type: ignore
-                    for branch in find_branches(self.repo, hexsha):
-                        stage = self.config.branch_to_stage(branch.name)
-                        if stage is None:
-                            continue
-                        state.artifacts[name].add_promotion(
-                            BasePromotion(
-                                artifact=index_artifact,
-                                version=version,
-                                stage=stage,
-                                creation_date=datetime.fromtimestamp(
-                                    commit.committed_date
-                                ),
-                                author=commit.author.name,
-                                commit_hexsha=commit.hexsha,
-                            )
-                        )
-        return state
-
-    def promote(
-        self,
-        name,
-        stage,
-        ref,
-        message=None,  # arg is ignored
-    ):
-        if self.config.VERSION_REQUIRED_FOR_STAGE:
-            # to promote, we need to register the version
-            # with this setting, the versions should be already registered in BaseRegistry.promote
-            # so we should not need to do anything here, except for maybe checking that it's done
-
-            # special case: if you are trying to promote a commit
-            # which has a version already registered and you don't want to deprecate it,
-            # then you need to create new commit with a model you want to use and register a version there
-            return None
-
-        # to promote, we don't need anything
-        if self.repo.heads[self.config.stage_to_branch(stage)].commit.hexsha == ref:
-            logging.info(
-                "HEAD commit in BRANCH is promoted to STAGE by default, so you don't need to run 'promote' command"
-            )
-            return None
-        # except maybe creating a commit in the branch (do we want this?):
-        # "gto promote $MODEL $PATH staging" -> create commit in branch for "staging"
-        raise NotImplementedError(
-            "If you want to promote a REF which is not HEAD of the appropriate branch, "
-            "you need to create a new commit to that branch "
-            "or move HEAD of it to the REF you want to promote"
-        )
-
-    def check_ref(self, ref: str, state: BaseRegistryState) -> Dict[str, BasePromotion]:
-        # TODO: we assume ref is a commit. If it's a tag then we don't need to return anything
-        # this is my assumption that should be discussed
-        # it's based on case when CI will be triggered twice - for registration tag and promotion commit
-        # VERSION_BASE='tag' VERSION_REQUIRED_FOR_STAGE=True STAGE_BASE='branch'
-        try:
-            assert all(r.name != ref for r in self.repo.refs)
-            ref = self.repo.commit(ref).hexsha
-        except (git.BadName, AssertionError):
-            logging.warning("Reference is not a commit hexsha or it doesn't exist")
-            return {}
-        return {
-            name: promotion
-            for name in state.artifacts
-            for promotion in state.artifacts[name].stages
-            if promotion.commit_hexsha == ref
-        }
diff --git a/gto/cli.py b/gto/cli.py
index face5da6..efa3c3e6 100644
--- a/gto/cli.py
+++ b/gto/cli.py
@@ -1,7 +1,6 @@
 import logging
 import sys
 from functools import wraps
-from typing import Sequence
 
 import click
 from tabulate import tabulate_formats
@@ -15,8 +14,9 @@
 
 
 class ALIAS:
-    REGISTER = ["register", "reg", "registration", "registrations"]
-    PROMOTE = ["promote", "prom", "promotion", "promotions"]
+    COMMIT = ["commit", "c"]
+    REGISTER = ["register", "r", "reg", "registration", "registrations"]
+    PROMOTE = ["promote", "p", "prom", "promotion", "promotions"]
 
 
 arg_name = click.argument(NAME)
@@ -26,6 +26,26 @@ class ALIAS:
 option_repo = click.option(
     "-r", "--repo", default=".", help="Repository to use", show_default=True
 )
+option_rev = click.option(
+    "--rev", default=None, help="Repo revision", show_default=True
+)
+option_discover = click.option(
+    "-d",
+    "--discover",
+    is_flag=True,
+    default=False,
+    help="Discover non-registered objects",
+)
+option_all_branches = click.option(
+    "-a",
+    "--all-branches",
+    is_flag=True,
+    default=False,
+    help="Read heads from all branches",
+)
+option_all_commits = click.option(
+    "-A", "--all-commits", is_flag=True, default=False, help="Read all commits"
+)
 option_format = click.option(
     "--format",
     "-f",
@@ -119,7 +139,7 @@ def decorator(func):
 
 @gto_command()
 @click.argument("repo", default=".")
-@click.option("--rev", default=None, help="Repo revision", show_default=True)
+@option_rev
 @click.option("--type", default=None, help="Artifact type to list", show_default=True)
 @click.option(
     "--json",
@@ -298,21 +318,49 @@ def check_ref(repo: str, ref: str, format: str):
 @gto_command()
 @option_repo
 @click.argument("object", default="registry")
+@option_discover
+@option_all_branches
+@option_all_commits
 @option_format_df
 @option_format_table
-def show(repo: str, object: str, format: str, format_table: str):
+def show(
+    repo: str,
+    object: str,
+    discover: bool,
+    all_branches,
+    all_commits,
+    format: str,
+    format_table: str,
+):
     """Show current registry state or specific artifact"""
     # TODO: make proper name resolving?
     # e.g. querying artifact named "registry" with artifact/registry
     if format == TABLE:
         format_echo(
-            gto.api.show(repo, object=object, table=True),
+            gto.api.show(
+                repo,
+                object=object,
+                discover=discover,
+                all_branches=all_branches,
+                all_commits=all_commits,
+                table=True,
+            ),
             format=format,
             format_table=format_table,
             if_empty="Nothing found in the current workspace",
         )
     else:
-        format_echo(gto.api.show(repo, object=object, table=False), format=format)
+        format_echo(
+            gto.api.show(
+                repo,
+                object=object,
+                discover=discover,
+                all_branches=all_branches,
+                all_commits=all_commits,
+                table=False,
+            ),
+            format=format,
+        )
 
 
 @gto_command(hidden=True)
@@ -374,55 +422,58 @@ def show_versions(repo, name, json, table, format_table):
 
 @gto_command()
 @option_repo
-@click.argument(
-    "action",
-    required=False,
-    type=click.Choice(ALIAS.REGISTER + ALIAS.PROMOTE),
-    nargs=-1,
-)
-@option_name
-@option_sort
-@option_format_table
-def audit(repo: str, action: Sequence[str], name: str, sort: str, format_table: str):
-    """Shows a journal of actions made in registry"""
-    if not action:
-        action = ALIAS.REGISTER[:1] + ALIAS.PROMOTE[:1]
-    if any(a in ALIAS.REGISTER for a in action):
-        click.echo("\n=== Registration audit trail ===")
-        format_echo(
-            gto.api.audit_registration(repo, name, sort, table=True),
-            format=TABLE,
-            format_table=format_table,
-            if_empty="No registered versions detected in the current workspace",
-        )
-
-    if any(a in ALIAS.PROMOTE for a in action):
-        click.echo("\n=== Promotion audit trail ===")
-        format_echo(
-            gto.api.audit_promotion(repo, name, sort, table=True),
-            format=TABLE,
-            format_table=format_table,
-            if_empty="No promotions detected in the current workspace",
-        )
-
-
-@gto_command()
-@option_repo
-@option_name
+@click.argument("name", required=False, default=None)
+@option_discover
+@option_all_branches
+@option_all_commits
+# @click.option(
+#     "--action",
+#     required=False,
+#     type=click.Choice(ALIAS.COMMIT + ALIAS.REGISTER + ALIAS.PROMOTE),
+#     nargs=-1,
+# )
 @option_format_df
 @option_format_table
 @option_sort
-def history(repo: str, name: str, format: str, format_table: str, sort: str):
+def history(
+    repo: str,
+    name: str,
+    discover,
+    all_branches,
+    all_commits,
+    format: str,
+    format_table: str,
+    sort: str,
+):
     """Show history of artifact"""
     if format == TABLE:
         format_echo(
-            gto.api.history(repo, name, sort, table=True),
+            gto.api.history(
+                repo,
+                name,
+                discover=discover,
+                all_branches=all_branches,
+                all_commits=all_commits,
+                sort=sort,
+                table=True,
+            ),
             format=format,
             format_table=format_table,
             if_empty="No history found",
         )
     else:
-        format_echo(gto.api.history(repo, name, sort, table=False), format=format)
+        format_echo(
+            gto.api.history(
+                repo,
+                name,
+                discover=discover,
+                all_branches=all_branches,
+                all_commits=all_commits,
+                sort=sort,
+                table=False,
+            ),
+            format=format,
+        )
 
 
 @gto_command()
@@ -462,5 +513,16 @@ def print_index(repo: str, format: str):
     format_echo(index, format)
 
 
+@gto_command()
+@option_repo
+@arg_name
+@option_rev
+def describe(repo, name: str, rev):
+    """Find enrichments for the artifact"""
+    infos = gto.api.describe(repo=repo, name=name, rev=rev)
+    for info in infos:
+        click.echo(info.get_human_readable())
+
+
 if __name__ == "__main__":
     cli()
diff --git a/gto/commit.py b/gto/commit.py
deleted file mode 100644
index ab26eea5..00000000
--- a/gto/commit.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import logging
-from datetime import datetime
-from typing import Dict, FrozenSet
-
-from git import BadName
-
-from .base import BaseManager, BaseRegistryState, BaseVersion
-from .constants import Action
-
-
-class CommitVersionManager(BaseManager):
-    actions: FrozenSet[Action] = frozenset((Action.PROMOTE,))
-
-    def update_state(self, state: BaseRegistryState) -> BaseRegistryState:
-        # each commit is a version if artifact is indexed in that commit
-        for name, artifact in state.artifacts.items():
-            for hexsha, index_artifact in artifact.commits.items():
-                commit = self.repo.commit(hexsha)
-                state.artifacts[name].versions.append(
-                    BaseVersion(
-                        artifact=index_artifact,
-                        name=hexsha,
-                        creation_date=datetime.fromtimestamp(commit.committed_date),
-                        author=commit.author.name,
-                        commit_hexsha=hexsha,
-                    )
-                )
-        return state
-
-    def check_ref(self, ref: str, state: BaseRegistryState) -> Dict[str, BaseVersion]:
-        try:
-            # check this is a commit and it exists
-            assert all(r.name != ref for r in self.repo.refs)
-            ref = self.repo.commit(ref).hexsha
-        except (BadName, AssertionError):
-            logging.warning("Reference is not a commit hexsha or it doesn't exist")
-            return {}
-        return {
-            name: version
-            for name in state.artifacts
-            for version in state.artifacts[name].versions
-            if version.commit_hexsha == ref
-        }
diff --git a/gto/config.py b/gto/config.py
index 0610806b..1f923b1f 100644
--- a/gto/config.py
+++ b/gto/config.py
@@ -2,24 +2,28 @@
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
-from pydantic import BaseSettings
+from pydantic import BaseModel, BaseSettings
 from pydantic.env_settings import InitSettingsSource
 from ruamel.yaml import YAML
 
-from gto.constants import BRANCH, COMMIT, TAG
+from gto.constants import TAG
 from gto.exceptions import UnknownStage, UnknownType
-from gto.versions import AbstractVersion
+
+from .constants import TAG
+from .ext import Enrichment, find_enrichment_types, find_enrichments
 
 yaml = YAML(typ="safe", pure=True)
 yaml.default_flow_style = False
 
-CONFIG_FILE = ".gto"
+CONFIG_FILE_NAME = ".gto"
 
 
 def _set_location_init_source(init_source: InitSettingsSource):
     def inner(settings: "RegistryConfig"):
-        if "CONFIG_FILE" in init_source.init_kwargs:
-            settings.__dict__["CONFIG_FILE"] = init_source.init_kwargs["CONFIG_FILE"]
+        if "CONFIG_FILE_NAME" in init_source.init_kwargs:
+            settings.__dict__["CONFIG_FILE_NAME"] = init_source.init_kwargs[
+                "CONFIG_FILE_NAME"
+            ]
         return {}
 
     return inner
@@ -31,7 +35,7 @@ def config_settings_source(settings: "RegistryConfig") -> Dict[str, Any]:
     """
 
     encoding = settings.__config__.env_file_encoding
-    config_file = getattr(settings, "CONFIG_FILE", CONFIG_FILE)
+    config_file = getattr(settings, "CONFIG_FILE_NAME", CONFIG_FILE_NAME)
     if not isinstance(config_file, Path):
         config_file = Path(config_file)
     if not config_file.exists():
@@ -41,18 +45,23 @@ def config_settings_source(settings: "RegistryConfig") -> Dict[str, Any]:
     return {k.upper(): v for k, v in conf.items()} if conf else {}
 
 
+class EnrichmentConfig(BaseModel):
+    type: str
+    config: Dict = {}
+
+    def load(self) -> Enrichment:
+        return find_enrichment_types()[self.type](**self.config)
+
+
 class RegistryConfig(BaseSettings):
     INDEX: str = "artifacts.yaml"
     TYPE_ALLOWED: List[str] = []
-    VERSION_BASE: str = TAG
-    VERSION_CONVENTION: str = "numbers"
-    VERSION_REQUIRED_FOR_STAGE: bool = True
-    STAGE_BASE: str = TAG
     STAGE_ALLOWED: List[str] = []
-    STAGE_BRANCH_MAPPING: Dict[str, str] = {}
     LOG_LEVEL: str = "INFO"
     DEBUG: bool = False
-    CONFIG_FILE: Optional[str] = CONFIG_FILE
+    ENRICHMENTS: List[EnrichmentConfig] = []
+    AUTOLOAD_ENRICHMENTS: bool = True
+    CONFIG_FILE_NAME: Optional[str] = CONFIG_FILE_NAME
 
     def assert_type(self, name):
         if not self.check_type(name):
@@ -62,24 +71,47 @@ def check_type(self, name):
         return name in self.TYPE_ALLOWED or not self.TYPE_ALLOWED
 
     @property
-    def VERSION_SYSTEM_MAPPING(self):
-        from .versions import NumberedVersion, SemVer
+    def VERSION_BASE(self):
+        return TAG
+
+    @property
+    def STAGE_BASE(self):
+        return TAG
+
+    @property
+    def VERSION_CLS(self):
+        from .versions import SemVer
 
-        return {"numbers": NumberedVersion, "semver": SemVer}
+        return SemVer
 
     @property
     def VERSION_MANAGER_CLS(self):
-        from .commit import CommitVersionManager
+        # from .commit import CommitVersionManager
         from .tag import TagVersionManager
 
-        return {COMMIT: CommitVersionManager, TAG: TagVersionManager}[self.VERSION_BASE]
+        return {TAG: TagVersionManager}[self.VERSION_BASE]
 
     @property
     def STAGE_MANAGER_CLS(self):
-        from .branch import BranchStageManager
+        # from .branch import BranchStageManager
         from .tag import TagStageManager
 
-        return {TAG: TagStageManager, BRANCH: BranchStageManager}[self.STAGE_BASE]
+        return {
+            TAG: TagStageManager,
+        }[self.STAGE_BASE]
+
+    @property
+    def ENRICHMENT_MANAGER_CLS(self):
+        from .index import EnrichmentManager
+
+        return EnrichmentManager
+
+    @property
+    def enrichments(self) -> Dict[str, Enrichment]:
+        res = {e.source: e for e in (e.load() for e in self.ENRICHMENTS)}
+        if self.AUTOLOAD_ENRICHMENTS:
+            return {**find_enrichments(), **res}
+        return res
 
     def assert_stage(self, name):
         if not self.check_stage(name):
@@ -90,22 +122,7 @@ def check_stage(self, name):
 
     @property
     def stages(self) -> List[str]:
-        if self.STAGE_BASE == TAG:
-            return self.STAGE_ALLOWED
-        if self.STAGE_BASE == BRANCH:
-            return list(self.STAGE_BRANCH_MAPPING)
-        raise NotImplementedError("Unknown STAGE_BASE")
-
-    def branch_to_stage(self, branch_name):
-        if self.STAGE_BRANCH_MAPPING:
-            return self.STAGE_BRANCH_MAPPING[branch_name]
-        return branch_name
-
-    def stage_to_branch(self, stage_name):
-        if self.STAGE_BRANCH_MAPPING:
-            return {value: key for key, value in self.STAGE_BRANCH_MAPPING.items()}[
-                stage_name
-            ]
+        return self.STAGE_ALLOWED
 
     class Config:
         env_prefix = "gto_"
@@ -164,11 +181,5 @@ def customise_sources(
     #         )
     #     return value
 
-    @property
-    def versions_class(self) -> AbstractVersion:
-        if self.VERSION_CONVENTION not in self.VERSION_SYSTEM_MAPPING:
-            raise ValueError(f"Unknown versioning system {self.VERSION_CONVENTION}")
-        return self.VERSION_SYSTEM_MAPPING[self.VERSION_CONVENTION]
-
 
 CONFIG = RegistryConfig()
diff --git a/gto/constants.py b/gto/constants.py
index 65709207..0adee29f 100644
--- a/gto/constants.py
+++ b/gto/constants.py
@@ -1,10 +1,10 @@
 from enum import Enum
 
 REF = "ref"
-COMMIT = "commit"
+# COMMIT = "commit"
 TAG = "tag"
-BRANCH = "branch"
-FILE = "file"
+# BRANCH = "branch"
+# FILE = "file"
 
 ACTION = "action"
 TYPE = "type"
diff --git a/gto/exceptions.py b/gto/exceptions.py
index bd6c050a..87f68c44 100644
--- a/gto/exceptions.py
+++ b/gto/exceptions.py
@@ -46,7 +46,7 @@ def __init__(self, name) -> None:
 
 
 class ArtifactNotFound(GTOException):
-    _message = "Artifact '{name}' doesn't exist in Index"
+    _message = "Cannot find artifact"
 
     def __init__(self, name) -> None:
         self.message = self._message.format(name=name)
diff --git a/gto/ext.py b/gto/ext.py
new file mode 100644
index 00000000..60106c1c
--- /dev/null
+++ b/gto/ext.py
@@ -0,0 +1,115 @@
+from abc import ABC, abstractmethod
+from functools import lru_cache
+from importlib import import_module
+from typing import Dict, Optional, Type
+
+import entrypoints
+from pydantic import BaseModel
+
+ENRICHMENT_ENRTYPOINT = "gto.enrichment"
+
+
+def import_string(path):
+    split = path.split(".")
+    module_name, object_name = ".".join(split[:-1]), split[-1]
+    mod = import_module(module_name)
+    try:
+        return getattr(mod, object_name)
+    except AttributeError as e:
+        raise ImportError(f"No object {object_name} in module {module_name}") from e
+
+
+class EnrichmentInfo(BaseModel, ABC):
+    source: str
+
+    @abstractmethod
+    def get_object(self) -> BaseModel:
+        raise NotImplementedError
+
+    def get_dict(self):
+        return self.get_object().dict()
+
+    @abstractmethod
+    def get_human_readable(self) -> str:
+        raise NotImplementedError
+
+
+class Enrichment(BaseModel, ABC):
+    source: str
+
+    @abstractmethod
+    def describe(
+        self, repo: str, obj: str, rev: Optional[str]
+    ) -> Optional[EnrichmentInfo]:
+        raise NotImplementedError
+
+
+# class CLIEnrichmentInfo(EnrichmentInfo):
+#     data: Dict
+#     repr: str
+
+#     def get_object(self) -> BaseModel:
+#         return self
+
+#     def get_human_readable(self) -> str:
+#         return self.repr
+
+
+# class CLIEnrichment(Enrichment):
+#     cmd: str
+#     info_type: Union[str, Type[EnrichmentInfo]] = CLIEnrichmentInfo
+
+#     @validator("info_type")
+#     def info_class_validator(
+#         cls, value
+#     ):  # pylint: disable=no-self-argument,no-self-use  # noqa: B902
+#         if isinstance(value, type):
+#             return value
+#         info_class = import_string(value)
+#         if not isinstance(info_class, type) or not issubclass(
+#             info_class, EnrichmentInfo
+#         ):
+#             raise ValueError(
+#                 "Wrong value for info_type: should be class or string path to class (e.g. `package.module.ClassName`)"
+#             )
+#         return info_class
+
+#     @property
+#     def info_class(self) -> Type[EnrichmentInfo]:
+#         return self.info_class_validator(self.info_type)
+
+#     def describe(self, obj: str) -> Optional[EnrichmentInfo]:
+#         try:
+#             data = loads(subprocess.check_output(self.cmd.split() + [obj]))
+#             return parse_obj_as(self.info_class, data)
+#         except subprocess.SubprocessError:
+#             return None
+
+
+@lru_cache()
+def _find_enrichments():
+    eps = entrypoints.get_group_named(ENRICHMENT_ENRTYPOINT)
+    return {k: ep.load() for k, ep in eps.items()}
+
+
+@lru_cache()
+def find_enrichments() -> Dict[str, Enrichment]:
+    enrichments = _find_enrichments()
+    res = {}
+    for name, e in enrichments.items():
+        # if isinstance(e, type) and issubclass(e, Enrichment) and not e.__fields_set__:
+        if isinstance(e, type) and issubclass(e, Enrichment):
+            res[name] = e()
+        if isinstance(e, Enrichment):
+            res[name] = e
+    return res
+
+
+@lru_cache()
+def find_enrichment_types() -> Dict[str, Type[Enrichment]]:
+    enrichments = _find_enrichments()
+    return {
+        k: e
+        for k, e in enrichments.items()
+        if isinstance(e, type) and issubclass(e, Enrichment)
+    }
diff --git a/gto/ext_dvc.py b/gto/ext_dvc.py
new file mode 100644
index 00000000..db6eb919
--- /dev/null
+++ b/gto/ext_dvc.py
@@ -0,0 +1,29 @@
+# from typing import Optional
+
+# from pydantic import BaseModel
+# from ruamel.yaml import safe_load
+
+# from gto.ext import Enrichment, EnrichmentInfo
+
+
+# class DVCEnrichmentInfo(EnrichmentInfo):
+#     source = "dvc"
+#     size: int
+#     hash: str
+
+#     def get_object(self) -> BaseModel:
+#         return self
+
+#     def get_human_readable(self) -> str:
+#         return f"""DVC-tracked [{self.size} bytes]"""
+
+
+# class DVCEnrichment(Enrichment):
+#     def describe(self, obj: str) -> Optional[DVCEnrichmentInfo]:
+#         try:
+#             with open(obj + ".dvc", encoding="utf8") as f:
+#                 dvc_data = safe_load(f)
+#                 data = dvc_data["outs"][0]
+#                 return DVCEnrichmentInfo(size=data["size"], hash=data["md5"])
+#         except FileNotFoundError:
+#             return None
diff --git a/gto/ext_mlem.py b/gto/ext_mlem.py
new file mode 100644
index 00000000..d946c2a7
--- /dev/null
+++ b/gto/ext_mlem.py
@@ -0,0 +1,39 @@
+# """This is temporary file that should be moved to mlem.gto module"""
+# from typing import Optional
+
+# from git import Repo
+# from mlem.core.errors import MlemObjectNotFound
+# from mlem.core.metadata import load_meta
+# from mlem.core.objects import DatasetMeta, MlemMeta, ModelMeta
+# from pydantic import BaseModel
+
+# from gto.ext import Enrichment, EnrichmentInfo
+
+
+# class MlemInfo(EnrichmentInfo):
+#     source = "mlem"
+#     meta: MlemMeta
+
+#     def get_object(self) -> BaseModel:
+#         return self.meta
+
+#     def get_human_readable(self) -> str:
+#         # TODO: create `.describe` method in MlemMeta https://github.com/iterative/mlem/issues/98
+#         description = f"""Mlem {self.meta.object_type}"""
+#         if isinstance(self.meta, ModelMeta):
+#             description += f": {self.meta.model_type.type}"
+#         if isinstance(self.meta, DatasetMeta):
+#             description += f": {self.meta.dataset.dataset_type.type}"
+#         return description
+
+
+# class MlemEnrichment(Enrichment):
+#     source = "mlem"
+
+#     def describe(self, repo, obj: str, rev: Optional[str]) -> Optional[MlemInfo]:
+#         try:
+#             if isinstance(repo, Repo):
+#                 repo = repo.working_dir
+#             return MlemInfo(meta=load_meta(obj, repo=repo))  # rev=rev
+#         except MlemObjectNotFound:
+#             return None
diff --git a/gto/index.py b/gto/index.py
index 66a236b1..9ce47e1a 100644
--- a/gto/index.py
+++ b/gto/index.py
@@ -1,15 +1,19 @@
 import os
 from abc import ABC, abstractmethod
 from collections import defaultdict
+from datetime import datetime
 from functools import wraps
 from pathlib import Path
-from typing import IO, Dict, Generator, List, Optional, Union
+from typing import IO, Dict, FrozenSet, Generator, List, Optional, Union
 
 import git
 from pydantic import BaseModel, parse_obj_as
 
-from .config import CONFIG_FILE, RegistryConfig, yaml
-from .exceptions import ArtifactExists, ArtifactNotFound, NoFile, NoRepo, PathIsUsed
+from gto.base import BaseManager, BaseRegistryState, BaseVersion
+from gto.config import CONFIG_FILE_NAME, RegistryConfig, yaml
+from gto.constants import Action
+from gto.exceptions import ArtifactExists, ArtifactNotFound, NoFile, NoRepo, PathIsUsed
+from gto.ext import Enrichment, EnrichmentInfo
 
 
 class Artifact(BaseModel):
@@ -154,7 +158,9 @@ class FileIndexManager(BaseIndexManager):
     @classmethod
     def from_path(cls, path: str, config: RegistryConfig = None):
         if config is None:
-            config = RegistryConfig(CONFIG_FILE=os.path.join(path, CONFIG_FILE))
+            config = RegistryConfig(
+                CONFIG_FILE_NAME=os.path.join(path, CONFIG_FILE_NAME)
+            )
         return cls(path=path, config=config)
 
     def index_path(self):
@@ -191,7 +197,7 @@ def from_repo(cls, repo: Union[str, git.Repo], config: RegistryConfig = None):
                 raise NoRepo(repo) from e
         if config is None:
             config = RegistryConfig(
-                CONFIG_FILE=os.path.join(repo.working_dir, CONFIG_FILE)
+                CONFIG_FILE_NAME=os.path.join(repo.working_dir, CONFIG_FILE_NAME)
             )
         return cls(repo=repo, config=config)
 
@@ -202,10 +208,17 @@ def index_path(self):
     class Config:
         arbitrary_types_allowed = True
 
-    def get_commit_index(self, ref: str) -> Index:
-        return Index.read(
-            (self.repo.commit(ref).tree / self.config.INDEX).data_stream, frozen=True
-        )
+    def get_commit_index(
+        self, ref: str, allow_to_not_exist: bool = True
+    ) -> Optional[Index]:
+        if self.config.INDEX in self.repo.commit(ref).tree:
+            return Index.read(
+                (self.repo.commit(ref).tree / self.config.INDEX).data_stream,
+                frozen=True,
+            )
+        if allow_to_not_exist:
+            return None
+        raise ValueError(f"No Index exists at {ref}")
 
     def get_history(self) -> Dict[str, Index]:
         commits = {
@@ -214,7 +227,7 @@ def get_history(self) -> Dict[str, Index]:
             for commit in traverse_commit(branch.commit)
         }
         return {
-            commit.hexsha: self.get_commit_index(commit.hexsha)
+            commit.hexsha: self.get_commit_index(commit.hexsha)  # type: ignore
             for commit in commits
             if self.config.INDEX in commit.tree
         }
@@ -234,8 +247,140 @@ def assert_existence(self, name, commit):
             raise ArtifactNotFound(name)
 
 
+class EnrichmentManager(BaseManager):
+    actions: FrozenSet[Action] = frozenset()
+
+    @classmethod
+    def from_repo(cls, repo, config: RegistryConfig = None):
+        if isinstance(repo, str):
+            repo = git.Repo(repo, search_parent_directories=True)
+        if config is None:
+            config = RegistryConfig(
+                CONFIG_FILE_NAME=os.path.join(repo.working_dir, CONFIG_FILE_NAME)
+            )
+        return cls(repo=repo, config=config)
+
+    def describe(self, name: str, rev: str = None) -> List[EnrichmentInfo]:
+
+        # TODO: add as arg?
+        config = RegistryConfig(
+            CONFIG_FILE_NAME=os.path.join(self.repo.working_dir, CONFIG_FILE_NAME)
+        )
+        enrichments = config.enrichments
+        res = []
+        gto_enrichment = enrichments.pop("gto")
+        gto_info = gto_enrichment.describe(self.repo, name, rev)
+        if gto_info:
+            res.append(gto_info)
+            path = gto_info.get_path()  # type: ignore
+            for enrichment in enrichments.values():
+                enrichment_data = enrichment.describe(self.repo, path, rev)
+                if enrichment_data is not None:
+                    res.append(enrichment_data)
+        return res
+
+    def get_commits(self, all_branches=False, all_commits=False):
+        if all_commits:
+            return {
+                commit
+                for branch in self.repo.heads
+                for commit in traverse_commit(branch.commit)
+            }
+        if all_branches:
+            return {branch.commit for branch in self.repo.heads}
+        return {self.repo.commit()}
+
+    def update_state(
+        self,
+        state: BaseRegistryState,
+        discover: bool = False,
+        all_branches=False,
+        all_commits=False,
+    ) -> BaseRegistryState:
+        # processing registered artifacts and versions first
+        for artifact in state.get_artifacts().values():
+            for version in artifact.versions:
+                enrichments = self.describe(artifact.name, rev=version.commit_hexsha)
+                artifact.update_enrichments(
+                    version=version.name, enrichments=enrichments
+                )
+                state.update_artifact(artifact)
+        # do discovery if requested
+        if discover:
+            for commit in self.get_commits(
+                all_branches=all_branches, all_commits=all_commits
+            ):
+                for enrichment in GTOEnrichment().discover(self.repo, commit):
+                    enrichments = self.describe(enrichment.artifact.name, rev=commit)
+                    artifact = state.find_artifact(
+                        enrichment.artifact.name, create_new=True
+                    )
+                    version = artifact.find_version(commit_hexsha=commit.hexsha)
+                    # TODO: duplicated in tag.py
+                    if version:
+                        version = version.name
+                    else:
+                        artifact.add_version(
+                            BaseVersion(
+                                artifact=enrichment.artifact.name,
+                                name=commit.hexsha,
+                                creation_date=datetime.fromtimestamp(
+                                    commit.committed_date
+                                ),
+                                author=commit.author.name,
+                                commit_hexsha=commit.hexsha,
+                                discovered=True,
+                            )
+                        )
+                        version = commit.hexsha
+                    artifact.update_enrichments(
+                        version=version, enrichments=enrichments
+                    )
+                    state.update_artifact(artifact)
+        return state
+
+    def check_ref(self, ref: str, state: BaseRegistryState):
+        # TODO: implement
+        raise NotImplementedError()
+
+
 def init_index_manager(path):
     try:
         return RepoIndexManager.from_repo(path)
     except NoRepo:
         return FileIndexManager.from_path(path)
+
+
+class GTOInfo(EnrichmentInfo):
+    source = "gto"
+    artifact: Artifact
+
+    def get_object(self) -> BaseModel:
+        return self.artifact
+
+    def get_human_readable(self) -> str:
+        description = f"""GTO artifact: {self.artifact}"""
+        return description
+
+    def get_path(self):
+        return self.artifact.path
+
+
+class GTOEnrichment(Enrichment):
+    source = "gto"
+
+    def discover(  # pylint: disable=no-self-use
+        self, repo, rev: Optional[str]
+    ) -> List[GTOInfo]:
+        index = RepoIndexManager.from_repo(repo).get_commit_index(rev)
+        if index:
+            return [GTOInfo(artifact=artifact) for artifact in index.state.values()]
+        return []
+
+    def describe(  # pylint: disable=no-self-use
+        self, repo, obj: str, rev: Optional[str]
+    ) -> Optional[GTOInfo]:
+        index = RepoIndexManager.from_repo(repo).get_commit_index(rev)
+        if obj in index.state:
+            return GTOInfo(artifact=index.state[obj])
+        return None
diff --git a/gto/registry.py b/gto/registry.py
index f84d5f75..0b48385e 100644
--- a/gto/registry.py
+++ b/gto/registry.py
@@ -6,21 +6,21 @@
 from git import InvalidGitRepositoryError, Repo
 from pydantic import BaseModel
 
-from gto.base import BaseArtifact, BaseManager, BasePromotion, BaseRegistryState
-from gto.config import CONFIG_FILE, RegistryConfig
+from gto.base import BaseManager, BasePromotion, BaseRegistryState
+from gto.config import CONFIG_FILE_NAME, RegistryConfig
 from gto.exceptions import (
     NoRepo,
     VersionAlreadyRegistered,
     VersionExistsForCommit,
     VersionIsOld,
 )
-from gto.index import RepoIndexManager
 
 
 class GitRegistry(BaseModel):
     repo: git.Repo
     version_manager: BaseManager
     stage_manager: BaseManager
+    enrichment_manager: BaseManager
     config: RegistryConfig
 
     class Config:
@@ -35,7 +35,7 @@ def from_repo(cls, repo=Union[str, Repo], config: RegistryConfig = None):
                 raise NoRepo(repo) from e
         if config is None:
             config = RegistryConfig(
-                CONFIG_FILE=os.path.join(repo.working_dir, CONFIG_FILE)
+                CONFIG_FILE_NAME=os.path.join(repo.working_dir, CONFIG_FILE_NAME)
             )
 
         return cls(
@@ -43,35 +43,46 @@ def from_repo(cls, repo=Union[str, Repo], config: RegistryConfig = None):
             config=config,
             version_manager=config.VERSION_MANAGER_CLS(repo=repo, config=config),
             stage_manager=config.STAGE_MANAGER_CLS(repo=repo, config=config),
+            enrichment_manager=config.ENRICHMENT_MANAGER_CLS(repo=repo, config=config),
         )
 
-    @property
-    def index(self):
-        return RepoIndexManager.from_repo(self.repo)
-
-    @property
-    def state(self) -> BaseRegistryState:
-        index = self.index.artifact_centric_representation()
-        state = BaseRegistryState(
-            artifacts={
-                name: BaseArtifact(
-                    name=name, commits=index[name], versions=[], stages=[]
-                )
-                for name in index
-            }
-        )
+    def get_state(
+        self, discover: bool = False, all_branches=False, all_commits=False
+    ) -> BaseRegistryState:
+        state = BaseRegistryState()
         state = self.version_manager.update_state(state)
         state = self.stage_manager.update_state(state)
+        state = self.enrichment_manager.update_state(  # type: ignore
+            state, discover=discover, all_branches=all_branches, all_commits=all_commits
+        )
         state.sort()
         return state
 
+    def get_artifacts(self, discover=False, all_branches=False, all_commits=False):
+        return self.get_state(
+            discover=discover, all_branches=all_branches, all_commits=all_commits
+        ).get_artifacts()
+
+    def find_artifact(
+        self,
+        name: str = None,
+        create_new=False,
+        discover=False,
+        all_branches=False,
+        all_commits=False,
+    ):
+        return self.get_state(
+            discover=discover, all_branches=all_branches, all_commits=all_commits
+        ).find_artifact(
+            name, create_new=create_new  # type: ignore
+        )
+
     def register(self, name, ref, version=None, bump=None):
         """Register artifact version"""
         ref = self.repo.commit(ref).hexsha
         # TODO: add the same check for other actions, to promote and etc
         # also we need to check integrity of the index+state
-        self.index.assert_existence(name, ref)
-        found_artifact = self.state.find_artifact(name)
+        found_artifact = self.get_state().find_artifact(name, create_new=True)
         # check that this commit don't have a version already
         found_version = found_artifact.find_version(commit_hexsha=ref)
         if found_version is not None:
@@ -80,30 +91,32 @@ def register(self, name, ref, version=None, bump=None):
         if version:
             if found_artifact.find_version(name=version) is not None:
                 raise VersionAlreadyRegistered(version)
-            if found_artifact.versions:
+            if found_artifact.discovered:
                 latest_ver = found_artifact.get_latest_version().name
-                if self.config.versions_class(version) < latest_ver:
+                if self.config.VERSION_CLS(version) < latest_ver:
                     raise VersionIsOld(latest=latest_ver, suggested=version)
         # if version name wasn't provided but there were some, bump the last one
-        elif found_artifact.versions:
+        elif found_artifact.discovered:
             version = (
-                self.config.versions_class(
-                    self.state.find_artifact(name).get_latest_version().name
+                self.config.VERSION_CLS(
+                    self.get_state().find_artifact(name).get_latest_version().name
                 )
                 .bump(**({"part": bump} if bump else {}))
                 .version
             )
         # if no versions exist, use the minimal version possible
         else:
-            version = self.config.versions_class.get_minimal().version
+            version = self.config.VERSION_CLS.get_minimal().version
         self.version_manager.register(
             name,
             version,
             ref,
             message=f"Registering artifact {name} version {version}",
         )
-        return self.state.find_artifact(name).find_version(
-            name=version, raise_if_not_found=True
+        return (
+            self.get_state()
+            .find_artifact(name)
+            .find_version(name=version, raise_if_not_found=True)
         )
 
     def promote(
@@ -120,9 +133,7 @@ def promote(
             raise ValueError("One and only one of (version, ref) must be specified.")
         if promote_ref:
             promote_ref = self.repo.commit(promote_ref).hexsha
-        if promote_ref:
-            self.index.assert_existence(name, promote_ref)
-        found_artifact = self.state.find_artifact(name)
+        found_artifact = self.get_state().find_artifact(name, create_new=True)
         if promote_version is not None:
             found_version = found_artifact.find_version(
                 name=promote_version, raise_if_not_found=True
@@ -141,25 +152,25 @@ def promote(
             ref=promote_ref,
             message=f"Promoting {name} version {promote_version} to stage {stage}",
         )
-        return self.state.find_artifact(name).promoted[stage]
+        return self.get_state().find_artifact(name).promoted[stage]
 
     def check_ref(self, ref: str):
         "Find out what was registered/promoted in this ref"
         return {
-            "version": self.version_manager.check_ref(ref, self.state),
-            "stage": self.stage_manager.check_ref(ref, self.state),
+            "version": self.version_manager.check_ref(ref, self.get_state()),
+            "stage": self.stage_manager.check_ref(ref, self.get_state()),
         }
 
     def find_commit(self, name, version):
-        return self.state.find_commit(name, version)
+        return self.get_state().find_commit(name, version)
 
     def which(self, name, stage, raise_if_not_found=True):
         """Return stage active in specific stage"""
-        return self.state.which(name, stage, raise_if_not_found)
+        return self.get_state().which(name, stage, raise_if_not_found)
 
     def latest(self, name: str):
         """Return latest active version for artifact"""
-        return self.state.find_artifact(name).get_latest_version()
+        return self.get_state().find_artifact(name).get_latest_version()
 
     def get_stages(self, in_use: bool = False):
         """Return list of stages in the registry.
@@ -168,8 +179,12 @@ def get_stages(self, in_use: bool = False):
         """
         if in_use:
             return {
-                stage for o in self.state.artifacts.values() for stage in o.promoted
+                stage
+                for o in self.get_state().artifacts.values()
+                for stage in o.promoted
             }
         return self.config.stages or {
-            stage for o in self.state.artifacts.values() for stage in o.unique_stages
+            stage
+            for o in self.get_state().artifacts.values()
+            for stage in o.unique_stages
         }
diff --git a/gto/tag.py b/gto/tag.py
index e50c52a1..fca7ef68 100644
--- a/gto/tag.py
+++ b/gto/tag.py
@@ -5,8 +5,6 @@
 import git
 from pydantic import BaseModel
 
-from gto.index import Artifact
-
 from .base import (
     BaseArtifact,
     BaseManager,
@@ -131,10 +129,10 @@ def create_tag(repo, name, ref, message):
     )
 
 
-def version_from_tag(artifact: Artifact, tag: git.Tag) -> BaseVersion:
+def version_from_tag(tag: git.Tag) -> BaseVersion:
     mtag = parse_tag(tag)
     return BaseVersion(
-        artifact=artifact,
+        artifact=mtag.name,
         name=mtag.version,
         creation_date=mtag.creation_date,
         author=tag.tag.tagger.name,
@@ -142,14 +140,32 @@ def version_from_tag(artifact: Artifact, tag: git.Tag) -> BaseVersion:
     )
 
 
-def promotion_from_tag(artifact: BaseArtifact, tag: git.Tag) -> BasePromotion:
+def promotion_from_tag(
+    artifact: BaseArtifact, tag: git.Tag, version_required: bool
+) -> BasePromotion:
     mtag = parse_tag(tag)
-    version = artifact.find_version(
-        commit_hexsha=tag.commit.hexsha, raise_if_not_found=True
-    )
+    if version_required:
+        version = artifact.find_version(
+            commit_hexsha=tag.commit.hexsha, raise_if_not_found=True
+        ).name  # type: ignore
+    else:
+        version = artifact.find_version(commit_hexsha=tag.commit.hexsha)
+        if version:
+            version = version.name  # type: ignore
+        else:
+            artifact.add_version(
+                BaseVersion(
+                    artifact=mtag.name,
+                    name=tag.commit.hexsha,
+                    creation_date=mtag.creation_date,
+                    author=tag.tag.tagger.name,
+                    commit_hexsha=tag.commit.hexsha,
+                )
+            )
+            version = tag.commit.hexsha
     return BasePromotion(
-        artifact=artifact.commits[tag.commit.hexsha],
-        version=version.name,  # type: ignore
+        artifact=mtag.name,
+        version=version,
         stage=mtag.stage,
         creation_date=mtag.creation_date,
         author=tag.tag.tagger.name,
@@ -157,17 +173,14 @@ def promotion_from_tag(artifact: BaseArtifact, tag: git.Tag) -> BasePromotion:
     )
 
 
-def index_tag(artifact: BaseArtifact, tag: git.Tag) -> BaseArtifact:
+def index_tag(
+    artifact: BaseArtifact, tag: git.Tag, version_required: bool
+) -> BaseArtifact:
     mtag = parse_tag(tag)
-    hexsha = mtag.tag.commit.hexsha
-    if hexsha not in artifact.commits:
-        # issue a warning that we're ignoring a tag,
-        # because artifact wasn't registered in that commit?
-        return artifact
     if mtag.action == Action.REGISTER:
-        artifact.versions.append(version_from_tag(artifact.commits[hexsha], tag))
+        artifact.add_version(version_from_tag(tag))
     if mtag.action == Action.PROMOTE:
-        artifact.add_promotion(promotion_from_tag(artifact, tag))
+        artifact.add_promotion(promotion_from_tag(artifact, tag, version_required))
     return artifact
 
 
@@ -177,7 +190,13 @@ def update_state(self, state: BaseRegistryState) -> BaseRegistryState:
         # this is important to check that history is not broken
         tags = [parse_tag(t) for t in find(repo=self.repo, action=self.actions)]
         for tag in tags:
-            state.artifacts[tag.name] = index_tag(state.artifacts[tag.name], tag.tag)
+            state.update_artifact(
+                index_tag(
+                    state.find_artifact(tag.name, create_new=True),
+                    tag.tag,
+                    version_required=False,
+                )
+            )
         return state
 
 
@@ -204,8 +223,8 @@ def check_ref(self, ref: str, state: BaseRegistryState):
             return {}
         return {
             name: version
-            for name in state.artifacts
-            for version in state.artifacts[name].versions
+            for name, artifact in state.get_artifacts().items()
+            for version in artifact.versions
             if name == art_name and version.name == version_name
         }
 
@@ -233,8 +252,8 @@ def check_ref(self, ref: str, state: BaseRegistryState):
             return {}
         return {
             name: promotion
-            for name in state.artifacts
-            for promotion in state.artifacts[name].stages
+            for name, artifact in state.get_artifacts().items()
+            for promotion in artifact.stages
             if name == art_name
             and promotion.commit_hexsha == tag.commit.hexsha
             and promotion.creation_date == datetime.fromtimestamp(tag.tag.tagged_date)
diff --git a/gto/versions.py b/gto/versions.py
index a051b5be..1e46a81b 100644
--- a/gto/versions.py
+++ b/gto/versions.py
@@ -4,7 +4,7 @@
 import semver
 
 from gto.constants import VersionPart
-from gto.exceptions import GTOException, IncomparableVersions, InvalidVersion
+from gto.exceptions import IncomparableVersions, InvalidVersion
 
 
 class AbstractVersion:
@@ -33,42 +33,6 @@ def get_minimal(cls):
         raise NotImplementedError
 
 
-@total_ordering
-class NumberedVersion(AbstractVersion):
-    @classmethod
-    def is_valid(cls, version):
-        if not isinstance(version, str):
-            return False
-        return version.startswith("v") and version[1:].isdigit()
-
-    def to_number(self):
-        return int(self.version[1:])
-
-    def __eq__(self, other):
-        if isinstance(other, str):
-            other = self.__class__(other)
-        if not isinstance(other, self.__class__):
-            raise IncomparableVersions(self, other)
-        return self.version == other.version
-
-    def __lt__(self, other):
-        if isinstance(other, str):
-            other = self.__class__(other)
-        if not isinstance(other, self.__class__):
-            raise IncomparableVersions(self, other)
-        return self.to_number() < other.to_number()
-
-    def bump(self, part: Union[VersionPart, str] = VersionPart.MAJOR):
-        part = VersionPart(part)
-        if part != VersionPart.MAJOR:
-            raise GTOException(f"With {self.__class__} you can only bump MAJOR part")
-        return self.__class__(f"v{self.to_number() + 1}")
-
-    @classmethod
-    def get_minimal(cls):
-        return cls("v1")
-
-
 @total_ordering
 class SemVer(AbstractVersion):
     """
diff --git a/setup.py b/setup.py
index a1dae8bc..f8cef96e 100644
--- a/setup.py
+++ b/setup.py
@@ -9,6 +9,7 @@
     "pydantic",
     "ruamel.yaml",
     "semver==3.0.0-dev.3",
+    "entrypoints",
     "tabulate==0.8.9",
 ]
 
@@ -53,6 +54,12 @@
     include_package_data=True,
     entry_points={
         "console_scripts": ["gto = gto.cli:cli"],
+        "gto.enrichment": [
+            # "mlem = gto.ext_mlem:MlemEnrichment",
+            # "dvc = gto.ext_dvc:DVCEnrichment",
+            # "cli = gto.ext:CLIEnrichment",
+            "gto = gto.index:GTOEnrichment",
+        ],
     },
     cmdclass={"build_py": build_py},
     zip_safe=False,


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ea9e1cbe..bfdf1343 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,7 +6,7 @@
 import pytest
 
 import gto
-from gto.config import CONFIG_FILE
+from gto.config import CONFIG_FILE_NAME
 
 
 @pytest.fixture
@@ -22,33 +22,13 @@ def write_file(name, content):
     return repo, write_file
 
 
-@pytest.fixture
-def init_showcase_numbers(empty_git_repo: Tuple[git.Repo, Callable]):
-    repo, write_file = empty_git_repo
-
-    write_file(
-        CONFIG_FILE,
-        """
-        version_base: tag
-        stage_base: tag
-        version_convention: numbers
-        """,
-    )
-
-    return repo, write_file
-
-
 @pytest.fixture
 def init_showcase_semver(empty_git_repo: Tuple[git.Repo, Callable]):
     repo, write_file = empty_git_repo
 
     write_file(
-        CONFIG_FILE,
-        """
-        version_base: tag
-        stage_base: tag
-        version_convention: semver
-        """,
+        CONFIG_FILE_NAME,
+        "",
     )
 
     return repo, write_file
diff --git a/tests/test_api.py b/tests/test_api.py
index 8bb7fdfd..e53ed4f4 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -23,8 +23,8 @@ def test_empty_state(empty_git_repo: Tuple[git.Repo, Callable]):
 def test_api_info_commands_empty_repo(empty_git_repo: Tuple[git.Repo, Callable]):
     repo, write_file = empty_git_repo  # pylint: disable=unused-variable
     gto.api.show(repo.working_dir)
-    gto.api.audit_registration(repo.working_dir)
-    gto.api.audit_promotion(repo.working_dir)
+    # gto.api.audit_registration(repo.working_dir)
+    # gto.api.audit_promotion(repo.working_dir)
     gto.api.history(repo.working_dir)
 
 
@@ -45,9 +45,9 @@ def test_add_remove(empty_git_repo: Tuple[git.Repo, Callable]):
 
 
 @pytest.fixture
-def repo_with_artifact(init_showcase_numbers):
+def repo_with_artifact(init_showcase_semver):
     repo: git.Repo
-    repo, write_file = init_showcase_numbers  # pylint: disable=unused-variable
+    repo, write_file = init_showcase_semver  # pylint: disable=unused-variable
     name, type, path_, virtual = "new-artifact", "new-type", "new/path", True
     gto.api.add(repo.working_dir, type, name, path_, virtual=virtual)
     repo.index.add(["artifacts.yaml"])
@@ -57,7 +57,7 @@ def repo_with_artifact(init_showcase_numbers):
 
 def test_register(repo_with_artifact):
     repo, name = repo_with_artifact
-    vname1, vname2 = "v1", "v2"
+    vname1, vname2 = "v1.0.0", "v1.0.1"
     gto.api.register(repo.working_dir, name, "HEAD", vname1)
     latest = gto.api.find_latest_version(repo.working_dir, name)
     assert latest.name == vname1
@@ -78,22 +78,15 @@ def test_promote(repo_with_artifact):
     repo, name = repo_with_artifact
     stage = "staging"
     gto.api.promote(
-        repo.working_dir, name, stage, promote_ref="HEAD", name_version="v1"
+        repo.working_dir, name, stage, promote_ref="HEAD", name_version="v0.0.1"
     )
     promotion = gto.api.find_promotion(repo.working_dir, name, stage)
     author = repo.commit().author.name
     _check_obj(
         promotion,
         dict(
-            artifact=dict(
-                type="new-type",
-                name=name,
-                path="new/path",
-                virtual=True,
-                tags=[],
-                description="",
-            ),
-            version="v1",
+            artifact=name,
+            version="v0.0.1",
             stage=stage,
             author=author,
             commit_hexsha=repo.commit().hexsha,
diff --git a/tests/test_config.py b/tests/test_config.py
index 29b5ab65..42db69dc 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,7 +1,7 @@
 import pytest
 
 from gto.api import add
-from gto.config import CONFIG_FILE
+from gto.config import CONFIG_FILE_NAME
 from gto.exceptions import UnknownType
 from gto.index import init_index_manager
 from gto.registry import GitRegistry
@@ -12,7 +12,7 @@ def init_repo(empty_git_repo):
     repo, write_file = empty_git_repo
 
     write_file(
-        CONFIG_FILE,
+        CONFIG_FILE_NAME,
         "type_allowed: [model, dataset]",
     )
     return repo
diff --git a/tests/test_registry.py b/tests/test_registry.py
index 60b350c7..b6f6da5a 100644
--- a/tests/test_registry.py
+++ b/tests/test_registry.py
@@ -7,28 +7,28 @@
 
 EXPECTED_REGISTRY_TAG_TAG_STATE = {
     "artifacts": {
-        "features": {
-            "name": "features",
-            "commits": {
-                "c6232f2ff38f2efe9768ab58b279e500085c0b0a": {
-                    "type": "dataset",
-                    "name": "features",
-                    "path": "datasets/features.csv",
-                    "virtual": True,
-                    "tags": [],
-                    "description": "",
-                },
-                "1570672b226558a3614c1a7f21295553102a2869": {
-                    "type": "dataset",
-                    "name": "features",
-                    "path": "datasets/features.csv",
-                    "virtual": True,
-                    "tags": [],
-                    "description": "",
-                },
-            },
-            "versions": [],
-        },
+        # "features": {
+        #     "name": "features",
+        #     "commits": {
+        #         "c6232f2ff38f2efe9768ab58b279e500085c0b0a": {
+        #             "type": "dataset",
+        #             "name": "features",
+        #             "path": "datasets/features.csv",
+        #             "virtual": True,
+        #             "tags": [],
+        #             "description": "",
+        #         },
+        #         "1570672b226558a3614c1a7f21295553102a2869": {
+        #             "type": "dataset",
+        #             "name": "features",
+        #             "path": "datasets/features.csv",
+        #             "virtual": True,
+        #             "tags": [],
+        #             "description": "",
+        #         },
+        #     },
+        #     "versions": [],
+        # },
         "nn": {
             "name": "nn",
             "commits": {
@@ -51,28 +51,14 @@
             },
             "versions": [
                 {
-                    "artifact": {
-                        "type": "model",
-                        "name": "nn",
-                        "path": "models/neural-network.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
+                    "artifact": "nn",
                     "name": "v0.0.1",
                     "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
                     "author": "Alexander Guschin",
                     "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
                     "promotions": [
                         {
-                            "artifact": {
-                                "type": "model",
-                                "name": "nn",
-                                "path": "models/neural-network.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
+                            "artifact": "nn",
                             "version": "v0.0.1",
                             "stage": "staging",
                             "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
@@ -105,28 +91,14 @@
             },
             "versions": [
                 {
-                    "artifact": {
-                        "type": "model",
-                        "name": "rf",
-                        "path": "models/random-forest.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
+                    "artifact": "rf",
                     "name": "v1.2.3",
                     "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
                     "author": "Alexander Guschin",
                     "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
                     "promotions": [
                         {
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
+                            "artifact": "rf",
                             "version": "v1.2.3",
                             "stage": "production",
                             "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
@@ -134,14 +106,7 @@
                             "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
                         },
                         {
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
+                            "artifact": "rf",
                             "version": "v1.2.3",
                             "stage": "production",
                             "creation_date": datetime.datetime(2022, 4, 1, 17, 48, 3),
@@ -151,28 +116,14 @@
                     ],
                 },
                 {
-                    "artifact": {
-                        "type": "model",
-                        "name": "rf",
-                        "path": "models/random-forest.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
+                    "artifact": "rf",
                     "name": "v1.2.4",
                     "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
                     "author": "Alexander Guschin",
                     "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
                     "promotions": [
                         {
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
+                            "artifact": "rf",
                             "version": "v1.2.4",
                             "stage": "staging",
                             "creation_date": datetime.datetime(2022, 4, 1, 17, 48, 1),
@@ -180,14 +131,7 @@
                             "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
                         },
                         {
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
+                            "artifact": "rf",
                             "version": "v1.2.4",
                             "stage": "production",
                             "creation_date": datetime.datetime(2022, 4, 1, 17, 48, 2),
@@ -211,9 +155,8 @@ def iter_over(sequence):
 
 
 def _check_state(appread_state, expected_state, exclude):
-
-    for name in ["features", "nn", "rf"]:
-        for part in ["commits", "versions"]:
+    for name in expected_state["artifacts"]:
+        for part in ["versions"]:  # "commits"
             for appeared, expected in zip(
                 iter_over(appread_state["artifacts"][name][part]),
                 iter_over(expected_state["artifacts"][name][part]),
@@ -230,292 +173,19 @@ def _check_state(appread_state, expected_state, exclude):
 def test_registry_state_tag_tag(showcase):
     path, repo, write_file, first_commit, second_commit = showcase
     reg = GitRegistry.from_repo(repo)
-    state = reg.state.dict()
+    state = reg.get_state().dict()
 
     exclude = {
         "commits": [],
-        "versions": ["author", "creation_date", "commit_hexsha", "promotions"],
+        "versions": [
+            "author",
+            "creation_date",
+            "commit_hexsha",
+            "promotions",
+            "enrichments",
+            "discovered",
+        ],
         "promotions": ["author", "creation_date", "commit_hexsha"],
     }
 
     _check_state(state, EXPECTED_REGISTRY_TAG_TAG_STATE, exclude)
-
-
-EXPECTED_REGISTRY_COMMIT_BRANCH_STATE = {
-    "artifacts": {
-        "features": {
-            "name": "features",
-            "commits": {
-                "1570672b226558a3614c1a7f21295553102a2869": {
-                    "type": "dataset",
-                    "name": "features",
-                    "path": "datasets/features.csv",
-                    "virtual": True,
-                    "tags": [],
-                    "description": "",
-                },
-                "c6232f2ff38f2efe9768ab58b279e500085c0b0a": {
-                    "type": "dataset",
-                    "name": "features",
-                    "path": "datasets/features.csv",
-                    "virtual": True,
-                    "tags": [],
-                    "description": "",
-                },
-            },
-            "versions": [
-                {
-                    "artifact": {
-                        "type": "dataset",
-                        "name": "features",
-                        "path": "datasets/features.csv",
-                        "virtual": True,
-                        "tags": [],
-                        "description": "",
-                    },
-                    "name": "1570672b226558a3614c1a7f21295553102a2869",
-                    "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
-                    "author": "Alexander Guschin",
-                    "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
-                    "promotions": [
-                        {
-                            "artifact": {
-                                "type": "dataset",
-                                "name": "features",
-                                "path": "datasets/features.csv",
-                                "virtual": True,
-                                "tags": [],
-                                "description": "",
-                            },
-                            "version": "1570672b226558a3614c1a7f21295553102a2869",
-                            "stage": "master",
-                            "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
-                            "author": "Alexander Guschin",
-                            "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
-                        }
-                    ],
-                },
-                {
-                    "artifact": {
-                        "type": "dataset",
-                        "name": "features",
-                        "path": "datasets/features.csv",
-                        "virtual": True,
-                        "tags": [],
-                        "description": "",
-                    },
-                    "name": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                    "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
-                    "author": "Alexander Guschin",
-                    "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                    "promotions": [
-                        {
-                            "artifact": {
-                                "type": "dataset",
-                                "name": "features",
-                                "path": "datasets/features.csv",
-                                "virtual": True,
-                                "tags": [],
-                                "description": "",
-                            },
-                            "version": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                            "stage": "master",
-                            "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
-                            "author": "Alexander Guschin",
-                            "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                        }
-                    ],
-                },
-            ],
-        },
-        "nn": {
-            "name": "nn",
-            "commits": {
-                "1570672b226558a3614c1a7f21295553102a2869": {
-                    "type": "model",
-                    "name": "nn",
-                    "path": "models/neural-network.pkl",
-                    "virtual": False,
-                    "tags": [],
-                    "description": "",
-                },
-                "c6232f2ff38f2efe9768ab58b279e500085c0b0a": {
-                    "type": "model",
-                    "name": "nn",
-                    "path": "models/neural-network.pkl",
-                    "virtual": False,
-                    "tags": [],
-                    "description": "",
-                },
-            },
-            "versions": [
-                {
-                    "artifact": {
-                        "type": "model",
-                        "name": "nn",
-                        "path": "models/neural-network.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
-                    "name": "1570672b226558a3614c1a7f21295553102a2869",
-                    "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
-                    "author": "Alexander Guschin",
-                    "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
-                    "promotions": [
-                        {
-                            "artifact": {
-                                "type": "model",
-                                "name": "nn",
-                                "path": "models/neural-network.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
-                            "version": "1570672b226558a3614c1a7f21295553102a2869",
-                            "stage": "master",
-                            "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
-                            "author": "Alexander Guschin",
-                            "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
-                        }
-                    ],
-                },
-                {
-                    "artifact": {
-                        "type": "model",
-                        "name": "nn",
-                        "path": "models/neural-network.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
-                    "name": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                    "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
-                    "author": "Alexander Guschin",
-                    "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                    "promotions": [
-                        {
-                            "artifact": {
-                                "type": "model",
-                                "name": "nn",
-                                "path": "models/neural-network.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
-                            "version": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                            "stage": "master",
-                            "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
-                            "author": "Alexander Guschin",
-                            "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                        }
-                    ],
-                },
-            ],
-        },
-        "rf": {
-            "name": "rf",
-            "commits": {
-                "1570672b226558a3614c1a7f21295553102a2869": {
-                    "type": "model",
-                    "name": "rf",
-                    "path": "models/random-forest.pkl",
-                    "virtual": False,
-                    "tags": [],
-                    "description": "",
-                },
-                "c6232f2ff38f2efe9768ab58b279e500085c0b0a": {
-                    "type": "model",
-                    "name": "rf",
-                    "path": "models/random-forest.pkl",
-                    "virtual": False,
-                    "tags": [],
-                    "description": "",
-                },
-            },
-            "versions": [
-                {
-                    "artifact": {
-                        "type": "model",
-                        "name": "rf",
-                        "path": "models/random-forest.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
-                    "name": "1570672b226558a3614c1a7f21295553102a2869",
-                    "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
-                    "author": "Alexander Guschin",
-                    "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
-                    "promotions": [
-                        {
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
-                            "version": "1570672b226558a3614c1a7f21295553102a2869",
-                            "stage": "master",
-                            "creation_date": datetime.datetime(2022, 4, 1, 17, 47, 59),
-                            "author": "Alexander Guschin",
-                            "commit_hexsha": "1570672b226558a3614c1a7f21295553102a2869",
-                        }
-                    ],
-                },
-                {
-                    "artifact": {
-                        "type": "model",
-                        "name": "rf",
-                        "path": "models/random-forest.pkl",
-                        "virtual": False,
-                        "tags": [],
-                        "description": "",
-                    },
-                    "name": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                    "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
-                    "author": "Alexander Guschin",
-                    "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                    "promotions": [
-                        {
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "tags": [],
-                                "description": "",
-                            },
-                            "version": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                            "stage": "master",
-                            "creation_date": datetime.datetime(2022, 4, 1, 17, 48),
-                            "author": "Alexander Guschin",
-                            "commit_hexsha": "c6232f2ff38f2efe9768ab58b279e500085c0b0a",
-                        }
-                    ],
-                },
-            ],
-        },
-    }
-}
-
-
-def test_registry_state_commit_branch(showcase):
-    from gto.config import CONFIG  # pylint: disable=import-outside-toplevel
-
-    CONFIG.VERSION_BASE = "commit"
-    CONFIG.STAGE_BASE = "branch"
-
-    path, repo, write_file, first_commit, second_commit = showcase
-    reg = GitRegistry.from_repo(repo, CONFIG)
-    state = reg.state.dict()
-
-    exclude = {
-        "commits": [],
-        "versions": ["author", "creation_date", "commit_hexsha", "name", "promotions"],
-        "promotions": ["author", "creation_date", "commit_hexsha", "version"],
-    }
-
-    _check_state(state, EXPECTED_REGISTRY_COMMIT_BRANCH_STATE, exclude)
diff --git a/tests/test_showcase.py b/tests/test_showcase.py
index 0d4d4efc..794fe703 100644
--- a/tests/test_showcase.py
+++ b/tests/test_showcase.py
@@ -15,13 +15,13 @@ def test_api(showcase):
     ) = showcase
 
     artifacts = gto.api._get_state(path).artifacts  # pylint: disable=protected-access
-    assert set(artifacts.keys()) == {"features", "nn", "rf"}
-    assert isinstance(artifacts["features"], BaseArtifact)
-    _check_obj(
-        artifacts["features"],
-        dict(name="features", versions=[]),
-        ["commits"],
-    )
+    assert set(artifacts.keys()) == {"nn", "rf"}  # "features"
+    # assert isinstance(artifacts["features"], BaseArtifact)
+    # _check_obj(
+    #     artifacts["features"],
+    #     dict(name="features", versions=[]),
+    #     ["commits"],
+    # )
     nn_artifact = artifacts["nn"]
     assert isinstance(nn_artifact, BaseArtifact)
     assert nn_artifact.name == "nn"
@@ -32,19 +32,13 @@ def test_api(showcase):
     _check_obj(
         nn_version,
         dict(
-            artifact=dict(
-                type="model",
-                name="nn",
-                path="models/neural-network.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="nn",
             name="v0.0.1",
             author=author,
             commit_hexsha=first_commit.hexsha,
+            discovered=False,
         ),
-        {"creation_date", "promotions"},
+        {"creation_date", "promotions", "enrichments"},
     )
     assert len(nn_artifact.stages) == 1
     nn_promotion = nn_artifact.stages[0]
@@ -52,14 +46,7 @@ def test_api(showcase):
     _check_obj(
         nn_promotion,
         dict(
-            artifact=dict(
-                type="model",
-                name="nn",
-                path="models/neural-network.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="nn",
             version="v0.0.1",
             stage="staging",
             author=author,
@@ -78,36 +65,24 @@ def test_api(showcase):
     _check_obj(
         rf_ver1,
         dict(
-            artifact=dict(
-                type="model",
-                name="rf",
-                path="models/random-forest.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="rf",
             name="v1.2.3",
             author=author,
             commit_hexsha=first_commit.hexsha,
+            discovered=False,
         ),
-        {"creation_date", "promotions"},
+        {"creation_date", "promotions", "enrichments"},
     )
     _check_obj(
         rf_ver2,
         dict(
-            artifact=dict(
-                type="model",
-                name="rf",
-                path="models/random-forest.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="rf",
             name="v1.2.4",
             author=author,
             commit_hexsha=second_commit.hexsha,
+            discovered=False,
         ),
-        {"creation_date", "promotions"},
+        {"creation_date", "promotions", "enrichments"},
     )
 
     assert len(rf_artifact.stages) == 4
@@ -118,14 +93,7 @@ def test_api(showcase):
     _check_obj(
         rf_l1,
         dict(
-            artifact=dict(
-                type="model",
-                name="rf",
-                path="models/random-forest.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="rf",
             version="v1.2.3",
             stage="production",
             author=author,
@@ -136,14 +104,7 @@ def test_api(showcase):
     _check_obj(
         rf_l4,
         dict(
-            artifact=dict(
-                type="model",
-                name="rf",
-                path="models/random-forest.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="rf",
             version="v1.2.4",
             stage="production",
             author=author,
@@ -154,14 +115,7 @@ def test_api(showcase):
     _check_obj(
         rf_l3,
         dict(
-            artifact=dict(
-                type="model",
-                name="rf",
-                path="models/random-forest.pkl",
-                virtual=False,
-                tags=[],
-                description="",
-            ),
+            artifact="rf",
             version="v1.2.4",
             stage="staging",
             author=author,
diff --git a/tests/test_versions.py b/tests/test_versions.py
index b757ed8e..76e5011c 100644
--- a/tests/test_versions.py
+++ b/tests/test_versions.py
@@ -1,27 +1,6 @@
 import pytest
 
-from gto.versions import NumberedVersion, SemVer
-
-
-@pytest.mark.parametrize("version", ["v0", "v1"])
-def test_number_version_is_valid(version):
-    assert NumberedVersion.is_valid(version)
-
-
-@pytest.mark.parametrize("version", [1, "1", "v-1", "-v1", "v1x", "v1.", "v1.0"])
-def test_number_version_is_not_valid(version):
-    assert not NumberedVersion.is_valid(version)
-
-
-def test_numbered_version_comparison():
-    assert NumberedVersion("v1") < NumberedVersion("v2")
-    assert NumberedVersion("v1") == NumberedVersion("v1")
-    assert NumberedVersion("v1") < "v2"
-    assert NumberedVersion("v1") == "v1"
-
-
-def test_bump_numbered_version():
-    assert NumberedVersion("v1").bump() == NumberedVersion("v2")
+from gto.versions import SemVer
 
 
 @pytest.mark.parametrize(