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>
This commit is contained in:
10
markitect/prompts/services/__init__.py
Normal file
10
markitect/prompts/services/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Service layer for Prompt Dependency Resolution.
|
||||
|
||||
This package provides high-level business logic for artifact and template
|
||||
management, orchestrating repositories and domain operations.
|
||||
"""
|
||||
|
||||
from markitect.prompts.services.artifact_service import ArtifactService
|
||||
|
||||
__all__ = ["ArtifactService"]
|
||||
239
markitect/prompts/services/artifact_service.py
Normal file
239
markitect/prompts/services/artifact_service.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user