155 lines
5.0 KiB
Python
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 = ...
|