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>
330 lines
11 KiB
Python
330 lines
11 KiB
Python
"""
|
|
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(),
|
|
}
|