feat(spaces): implement Phase 0-1 of Information Space Service
Phase 0 - Project Organization: - Create docs/PROJECT_STRUCTURE.md documenting codebase layout - Create markitect/core/ with parser, serializer, document_manager, workspace - Create markitect/schema/ consolidating 6 schema_*.py modules - Create markitect/storage/ with database module - Maintain backward compatibility via re-exports from original locations - Add docs/roadmap/information-space-service/ with README and WORKPLAN Phase 1 - Foundation (Weeks 1-3): - Week 1: Core domain models (InformationSpace, SpaceDocument, SpaceConfig, SpaceMetadata, SpaceVariable, TransclusionReference, SpaceStatus) - Week 2: Repository layer with interfaces (ISpaceRepository, IDocumentAssociationRepository, IVariableRepository, IReferenceRepository) and SQLite implementations with foreign key cascade deletes - Week 3: SpaceService orchestration layer with full CRUD, document, variable, and reference tracking operations Test coverage: 124 tests (25 model + 63 repository + 36 integration) Capabilities delivered: - CAP-001: InformationSpace entity with lifecycle management - CAP-002: SpaceRepository CRUD with SQLite backing - CAP-003: Document-Space associations with path-based organization - CAP-004: Space metadata and configuration schemas - CAP-005: Database schema with migrations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
329
markitect/spaces/models.py
Normal file
329
markitect/spaces/models.py
Normal file
@@ -0,0 +1,329 @@
|
||||
"""
|
||||
Core domain models for Information Spaces.
|
||||
|
||||
This module provides the foundational data models for the Information Space
|
||||
abstraction, including the space entity, document associations, and configuration.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SpaceStatus(Enum):
|
||||
"""Lifecycle status of an Information Space."""
|
||||
DRAFT = "draft"
|
||||
ACTIVE = "active"
|
||||
ARCHIVED = "archived"
|
||||
DELETED = "deleted"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpaceMetadata:
|
||||
"""
|
||||
Extensible metadata for an Information Space.
|
||||
|
||||
Attributes:
|
||||
tags: List of tags for categorization
|
||||
author: Author identifier
|
||||
custom: Dictionary for custom metadata fields
|
||||
"""
|
||||
tags: List[str] = field(default_factory=list)
|
||||
author: Optional[str] = None
|
||||
custom: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert metadata to dictionary for serialization."""
|
||||
return {
|
||||
"tags": self.tags,
|
||||
"author": self.author,
|
||||
"custom": self.custom,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SpaceMetadata":
|
||||
"""Create metadata from dictionary."""
|
||||
return cls(
|
||||
tags=data.get("tags", []),
|
||||
author=data.get("author"),
|
||||
custom=data.get("custom", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpaceConfig:
|
||||
"""
|
||||
Configuration settings for an Information Space.
|
||||
|
||||
Attributes:
|
||||
default_variant: Default directory variant for export (flat/hierarchical/semantic)
|
||||
enable_caching: Whether to enable render caching
|
||||
theme: Theme name for HTML rendering
|
||||
history_enabled: Whether git history tracking is enabled (Phase 8)
|
||||
history_backend: History backend type (default: "git")
|
||||
history_options: Additional history backend options
|
||||
variable_scope: Default variable scope resolution strategy
|
||||
"""
|
||||
default_variant: str = "hierarchical"
|
||||
enable_caching: bool = True
|
||||
theme: Optional[str] = None
|
||||
history_enabled: bool = False
|
||||
history_backend: str = "git"
|
||||
history_options: Dict[str, Any] = field(default_factory=dict)
|
||||
variable_scope: str = "space" # space, document, request
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert config to dictionary for serialization."""
|
||||
return {
|
||||
"default_variant": self.default_variant,
|
||||
"enable_caching": self.enable_caching,
|
||||
"theme": self.theme,
|
||||
"history_enabled": self.history_enabled,
|
||||
"history_backend": self.history_backend,
|
||||
"history_options": self.history_options,
|
||||
"variable_scope": self.variable_scope,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SpaceConfig":
|
||||
"""Create config from dictionary."""
|
||||
return cls(
|
||||
default_variant=data.get("default_variant", "hierarchical"),
|
||||
enable_caching=data.get("enable_caching", True),
|
||||
theme=data.get("theme"),
|
||||
history_enabled=data.get("history_enabled", False),
|
||||
history_backend=data.get("history_backend", "git"),
|
||||
history_options=data.get("history_options", {}),
|
||||
variable_scope=data.get("variable_scope", "space"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpaceDocument:
|
||||
"""
|
||||
Represents a document's membership in an Information Space.
|
||||
|
||||
Attributes:
|
||||
id: Unique document membership identifier
|
||||
space_id: ID of the containing space
|
||||
document_id: Reference to the actual document
|
||||
space_path: Path within the space (e.g., "/intro.md")
|
||||
order_index: Ordering within the space
|
||||
metadata: Document-specific metadata
|
||||
content_hash: Hash of document content for change detection
|
||||
added_at: Timestamp when document was added
|
||||
"""
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
space_id: str = ""
|
||||
document_id: str = ""
|
||||
space_path: str = ""
|
||||
order_index: int = 0
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
content_hash: Optional[str] = None
|
||||
added_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert document association to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"space_id": self.space_id,
|
||||
"document_id": self.document_id,
|
||||
"space_path": self.space_path,
|
||||
"order_index": self.order_index,
|
||||
"metadata": self.metadata,
|
||||
"content_hash": self.content_hash,
|
||||
"added_at": self.added_at.isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SpaceDocument":
|
||||
"""Create document association from dictionary."""
|
||||
added_at = data.get("added_at")
|
||||
if isinstance(added_at, str):
|
||||
added_at = datetime.fromisoformat(added_at)
|
||||
elif added_at is None:
|
||||
added_at = datetime.now()
|
||||
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())),
|
||||
space_id=data.get("space_id", ""),
|
||||
document_id=data.get("document_id", ""),
|
||||
space_path=data.get("space_path", ""),
|
||||
order_index=data.get("order_index", 0),
|
||||
metadata=data.get("metadata", {}),
|
||||
content_hash=data.get("content_hash"),
|
||||
added_at=added_at,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InformationSpace:
|
||||
"""
|
||||
First-class Information Space abstraction.
|
||||
|
||||
An Information Space is a container for documents with transclusion
|
||||
relationships, persistent context, and lifecycle management.
|
||||
|
||||
Attributes:
|
||||
id: Unique space identifier
|
||||
name: Human-readable unique name
|
||||
description: Optional description
|
||||
metadata: Extensible metadata
|
||||
config: Space configuration
|
||||
parent_space_id: Optional parent space for inheritance
|
||||
status: Current lifecycle status
|
||||
created_at: Creation timestamp
|
||||
updated_at: Last update timestamp
|
||||
|
||||
Example:
|
||||
space = InformationSpace(
|
||||
name="api-docs",
|
||||
description="API Documentation",
|
||||
config=SpaceConfig(theme="technical")
|
||||
)
|
||||
"""
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
name: str = ""
|
||||
description: Optional[str] = None
|
||||
metadata: SpaceMetadata = field(default_factory=SpaceMetadata)
|
||||
config: SpaceConfig = field(default_factory=SpaceConfig)
|
||||
parent_space_id: Optional[str] = None
|
||||
status: SpaceStatus = SpaceStatus.DRAFT
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate space after initialization."""
|
||||
if not self.name:
|
||||
raise ValueError("Space name is required")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert space to dictionary for serialization."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"metadata": self.metadata.to_dict() if isinstance(self.metadata, SpaceMetadata) else self.metadata,
|
||||
"config": self.config.to_dict() if isinstance(self.config, SpaceConfig) else self.config,
|
||||
"parent_space_id": self.parent_space_id,
|
||||
"status": self.status.value if isinstance(self.status, SpaceStatus) else self.status,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "InformationSpace":
|
||||
"""Create space from dictionary."""
|
||||
created_at = data.get("created_at")
|
||||
if isinstance(created_at, str):
|
||||
created_at = datetime.fromisoformat(created_at)
|
||||
elif created_at is None:
|
||||
created_at = datetime.now()
|
||||
|
||||
updated_at = data.get("updated_at")
|
||||
if isinstance(updated_at, str):
|
||||
updated_at = datetime.fromisoformat(updated_at)
|
||||
elif updated_at is None:
|
||||
updated_at = datetime.now()
|
||||
|
||||
status = data.get("status", "draft")
|
||||
if isinstance(status, str):
|
||||
status = SpaceStatus(status)
|
||||
|
||||
metadata = data.get("metadata", {})
|
||||
if isinstance(metadata, dict):
|
||||
metadata = SpaceMetadata.from_dict(metadata)
|
||||
|
||||
config = data.get("config", {})
|
||||
if isinstance(config, dict):
|
||||
config = SpaceConfig.from_dict(config)
|
||||
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())),
|
||||
name=data["name"],
|
||||
description=data.get("description"),
|
||||
metadata=metadata,
|
||||
config=config,
|
||||
parent_space_id=data.get("parent_space_id"),
|
||||
status=status,
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
)
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Activate the space."""
|
||||
self.status = SpaceStatus.ACTIVE
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def archive(self) -> None:
|
||||
"""Archive the space."""
|
||||
self.status = SpaceStatus.ARCHIVED
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def touch(self) -> None:
|
||||
"""Update the last modified timestamp."""
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpaceVariable:
|
||||
"""
|
||||
Variable stored at space level for transclusion context.
|
||||
|
||||
Attributes:
|
||||
space_id: ID of the containing space
|
||||
name: Variable name
|
||||
value: Variable value (JSON-serializable)
|
||||
scope: Variable scope (space, document, request)
|
||||
"""
|
||||
space_id: str
|
||||
name: str
|
||||
value: Any
|
||||
scope: str = "space"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"space_id": self.space_id,
|
||||
"name": self.name,
|
||||
"value": self.value,
|
||||
"scope": self.scope,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SpaceVariable":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
space_id=data["space_id"],
|
||||
name=data["name"],
|
||||
value=data["value"],
|
||||
scope=data.get("scope", "space"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransclusionReference:
|
||||
"""
|
||||
Tracks a transclusion reference between documents for cache invalidation.
|
||||
|
||||
Attributes:
|
||||
source_doc_id: ID of the document containing the transclusion
|
||||
target_doc_id: ID of the transcluded document
|
||||
space_id: ID of the space containing the reference
|
||||
created_at: When the reference was created
|
||||
"""
|
||||
source_doc_id: str
|
||||
target_doc_id: str
|
||||
space_id: str
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"source_doc_id": self.source_doc_id,
|
||||
"target_doc_id": self.target_doc_id,
|
||||
"space_id": self.space_id,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
}
|
||||
Reference in New Issue
Block a user