Files
markitect-main/markitect/prompts/services/artifact_service.py
tegwick 945544880d feat(prompts): implement Phase 1 - Foundation (FR-1)
Implement addressable artifacts with content-based identity and change detection.

Core Features:
- Artifact model with SHA-256 content digests
- ArtifactReference for cross-space addressing
- IArtifactRepository interface for pluggable storage
- SQLiteArtifactRepository implementation
- ArtifactService for high-level operations
- Content digest calculation utilities

Database:
- prompt_artifacts table with indexes
- Support for artifact metadata and types
- UNIQUE constraint on space_id+name

Tests (41 passing):
- 26 model tests (metadata, artifacts, references, digests)
- 15 repository tests (CRUD, queries, constraints)

Implements:
- FR-1.1: Unique addressability by name and ID
- FR-1.2: Content digest computation and storage
- FR-1.3: Cross-space artifact references

Files Created:
- markitect/prompts/models.py
- markitect/prompts/repositories/interfaces.py
- markitect/prompts/repositories/sqlite.py
- markitect/prompts/services/artifact_service.py
- migrations/prompts/001_create_artifacts_table.sql
- tests/unit/prompts/test_artifact_models.py
- tests/unit/prompts/test_artifact_repository.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:30:26 +01:00

240 lines
6.5 KiB
Python

"""
Artifact service for high-level artifact management operations.
This service provides business logic for creating, querying, and updating
artifacts with automatic content digest calculation and change tracking.
"""
from typing import List, Optional
from datetime import datetime
from markitect.prompts.models import (
Artifact,
ArtifactType,
ArtifactMetadata,
ArtifactReference,
calculate_content_digest,
)
from markitect.prompts.repositories.interfaces import (
IArtifactRepository,
ArtifactNotFoundError,
)
class ArtifactService:
"""
Service for artifact management operations.
Provides high-level business logic on top of artifact repository,
handling content digest calculation, change detection, and cross-space
artifact resolution.
"""
def __init__(self, repository: IArtifactRepository):
"""
Initialize service with repository.
Args:
repository: Artifact repository implementation
"""
self.repository = repository
def create_artifact(
self,
space_id: str,
name: str,
content: str,
artifact_type: ArtifactType = ArtifactType.CONTENT,
metadata: Optional[ArtifactMetadata] = None,
) -> Artifact:
"""
Create a new artifact with automatic digest calculation.
Args:
space_id: ID of containing space
name: Artifact name
content: Artifact content
artifact_type: Type classification
metadata: Optional metadata
Returns:
Created artifact
Raises:
DuplicateArtifactError: If artifact already exists
"""
artifact = Artifact.create(
space_id=space_id,
name=name,
content=content,
artifact_type=artifact_type,
metadata=metadata,
)
return self.repository.create(artifact)
def get_artifact(self, artifact_id: str) -> Artifact:
"""
Retrieve artifact by ID.
Args:
artifact_id: Artifact identifier
Returns:
Artifact instance
Raises:
ArtifactNotFoundError: If artifact doesn't exist
"""
artifact = self.repository.get_by_id(artifact_id)
if not artifact:
raise ArtifactNotFoundError(f"Artifact with ID '{artifact_id}' not found")
return artifact
def get_artifact_by_name(self, space_id: str, name: str) -> Artifact:
"""
Retrieve artifact by space and name.
Args:
space_id: Space identifier
name: Artifact name
Returns:
Artifact instance
Raises:
ArtifactNotFoundError: If artifact doesn't exist
"""
artifact = self.repository.get_by_name(space_id, name)
if not artifact:
raise ArtifactNotFoundError(
f"Artifact '{name}' not found in space '{space_id}'"
)
return artifact
def resolve_reference(
self,
reference: ArtifactReference,
search_spaces: List[str],
) -> Optional[Artifact]:
"""
Resolve an artifact reference across multiple spaces.
Implements FR-1.3: Cross-space artifact references
Searches spaces in order until artifact is found.
Args:
reference: Artifact reference to resolve
search_spaces: List of space IDs to search in order
Returns:
Resolved artifact, or None if not found
"""
# If reference specifies space, search only that space
if reference.space_id:
return self.repository.get_by_name(reference.space_id, reference.name)
# Search spaces in order
for space_id in search_spaces:
artifact = self.repository.get_by_name(space_id, reference.name)
if artifact:
# TODO: Handle version constraint when versioning is implemented
return artifact
return None
def update_artifact_content(
self,
artifact_id: str,
new_content: str,
) -> Artifact:
"""
Update artifact content with automatic digest recalculation.
Args:
artifact_id: Artifact to update
new_content: New content
Returns:
Updated artifact
Raises:
ArtifactNotFoundError: If artifact doesn't exist
"""
artifact = self.get_artifact(artifact_id)
artifact.update_content(new_content)
return self.repository.update(artifact)
def detect_change(self, artifact_id: str, current_content: str) -> bool:
"""
Detect if artifact content has changed.
Args:
artifact_id: Artifact to check
current_content: Current content to compare
Returns:
True if content has changed
Raises:
ArtifactNotFoundError: If artifact doesn't exist
"""
artifact = self.get_artifact(artifact_id)
current_digest = calculate_content_digest(current_content)
return artifact.has_changed(current_digest)
def list_artifacts(
self,
space_id: str,
artifact_type: Optional[ArtifactType] = None,
) -> List[Artifact]:
"""
List artifacts in a space.
Args:
space_id: Space identifier
artifact_type: Optional type filter
Returns:
List of artifacts
"""
return self.repository.list_by_space(space_id, artifact_type)
def find_by_digest(self, content_digest: str) -> List[Artifact]:
"""
Find all artifacts with matching content digest.
Useful for finding duplicate content across spaces.
Args:
content_digest: SHA-256 digest to match
Returns:
List of artifacts with matching digest
"""
return self.repository.get_by_digest(content_digest)
def delete_artifact(self, artifact_id: str) -> bool:
"""
Delete an artifact.
Args:
artifact_id: Artifact to delete
Returns:
True if deleted, False if not found
"""
return self.repository.delete(artifact_id)
def artifact_exists(self, space_id: str, name: str) -> bool:
"""
Check if artifact exists.
Args:
space_id: Space identifier
name: Artifact name
Returns:
True if artifact exists
"""
return self.repository.exists(space_id, name)