""" 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(), }