"""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 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 = ...