Files
asf/packages/asf-domain/asf_domain/artifact.py
T
2026-09-08 07:21:34 -04:00

155 lines
5.0 KiB
Python

"""Artifact — the heart of the "Everything is an Artifact" principle.
One single generic model captures every lifecycle entity (requirement,
task, commit, test, deployment, incident, ...). Artifact subtypes are
expressed via the ``type`` enum / string and stored in the same MongoDB
``artifacts`` collection.
Relationships between artifacts live in ``artifact_relationships``
collection and are modelled as explicit directed edges — this keeps the
option open to project the same data onto a graph database later without
rewriting domain logic.
"""
from __future__ import annotations
from typing import Any, Optional, List, Dict
from enum import Enum
from pydantic import Field
from asf_domain.base import (
ASFBaseModel,
TenantScopedModel,
ASFFieldID,
UTCDatetime,
ArtifactType,
RelationshipType,
generate_id,
)
# ======================================================================
# Artifact itself
# ======================================================================
class ArtifactBase(ASFBaseModel):
type: ArtifactType = Field(
..., description="Kind of artifact (requirement | task | commit | …)"
)
external_id: Optional[str] = Field(
None,
max_length=255,
description="Optional identifier in an external system "
"(e.g. GitHub PR id, Jira issue key)",
)
title: str = Field(..., min_length=1, max_length=500)
content: Optional[str] = Field(
None,
description="Primary human readable body (markdown, description, diff, log body, …). "
"Large content should live in MinIO; store a reference here or in metadata.",
)
source: Optional[str] = Field(
None,
max_length=255,
description="Origin system: jira, github, manual, ai, …",
)
status: Optional[str] = Field(
None, max_length=80, description="Status (e.g. open, done, failed, released)."
)
class ArtifactCreate(ArtifactBase):
"""Create payload. ``tenant_id`` and ``project_id`` are set server-side."""
project_id: ASFFieldID = Field(
..., description="Owning project (validated against tenant context server side)."
)
metadata: Dict[str, Any] = Field(
default_factory=dict,
description="Arbitrary extensible fields. Use this for artifact subtype "
"specific attributes (e.g. commit.sha, test.outcome, deployment.env).",
)
class ArtifactUpdate(ASFBaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=500)
content: Optional[str] = None
type: Optional[ArtifactType] = None
external_id: Optional[str] = None
source: Optional[str] = None
status: Optional[str] = None
metadata: Optional[Dict[str, Any]] = Field(
None,
description="When provided, performs a shallow merge with existing metadata.",
)
class Artifact(TenantScopedModel, ArtifactBase):
"""Stored Artifact record.
``relationships`` is a denormalised, always-stale list kept for fast
reads in the UI. The authoritative source of truth for edges is the
``artifact_relationships`` collection.
"""
id: ASFFieldID = Field(default_factory=generate_id)
project_id: ASFFieldID = ...
version: int = Field(
1, ge=1, description="Monotonic version bumped on every update."
)
metadata: Dict[str, Any] = Field(default_factory=dict)
created_at: UTCDatetime = ...
updated_at: UTCDatetime = ...
created_by: Optional[ASFFieldID] = Field(
None, description="User id that created this artifact (null = system / import)."
)
# ======================================================================
# ArtifactRelationship — the explicit edge
# ======================================================================
class RelationshipDirection(str, Enum):
FORWARD = "forward"
REVERSE = "reverse"
class ArtifactRelationshipBase(ASFBaseModel):
relationship_type: RelationshipType = Field(
...,
description="Semantic predicate: ``source <relationship_type> target``.",
)
source_artifact_id: ASFFieldID = Field(
..., description="Source artifact id (subject)."
)
target_artifact_id: ASFFieldID = Field(
..., description="Target artifact id (object)."
)
metadata: Dict[str, Any] = Field(
default_factory=dict,
description="Free-form metadata on the edge (confidence, evidence ref, …).",
)
class ArtifactRelationshipCreate(ArtifactRelationshipBase):
"""Create payload. tenant_id / project_ids are validated server side."""
pass
class ArtifactRelationship(TenantScopedModel, ArtifactRelationshipBase):
"""Stored edge between two artifacts.
Invariant: both source and target MUST belong to the same tenant.
They MAY live in different projects inside that tenant when an
explicit cross-project reference is intentional.
"""
id: ASFFieldID = Field(default_factory=generate_id)
source_project_id: ASFFieldID = ...
target_project_id: ASFFieldID = ...
created_at: UTCDatetime = ...
updated_at: UTCDatetime = ...