first
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""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 = ...
|
||||
Reference in New Issue
Block a user