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:
2026-02-08 02:02:46 +01:00
parent 6ebcc0f60e
commit 9b12875681
45 changed files with 9818 additions and 4300 deletions

View File

@@ -0,0 +1,50 @@
"""
Core infrastructure modules for MarkiTect.
This package contains the fundamental building blocks:
- Parser: Markdown to AST conversion
- Serializer: AST to Markdown serialization
- DocumentManager: Document ingestion and management
- Workspace: Workspace and project management
"""
from .parser import parse_markdown_to_ast
from .serializer import ASTSerializer
from .document_manager import DocumentManager, CleanDocumentManager
from .workspace import (
WorkspaceManager,
WorkspaceTemplate,
TemplateMetadata,
TemplateResult,
WorkspaceCreationResult,
ProjectResult,
SyncResult,
BackupResult,
RestoreResult,
WorkspaceState,
ConflictInfo,
MergeResult,
)
__all__ = [
# Parser
"parse_markdown_to_ast",
# Serializer
"ASTSerializer",
# Document Manager
"DocumentManager",
"CleanDocumentManager",
# Workspace
"WorkspaceManager",
"WorkspaceTemplate",
"TemplateMetadata",
"TemplateResult",
"WorkspaceCreationResult",
"ProjectResult",
"SyncResult",
"BackupResult",
"RestoreResult",
"WorkspaceState",
"ConflictInfo",
"MergeResult",
]

View File

@@ -0,0 +1,98 @@
"""
Document manager - Clean implementation.
This module provides the DocumentManager class which is now a wrapper around
the CleanDocumentManager for backward compatibility.
"""
from markitect.clean_document_manager import CleanDocumentManager
from .parser import parse_markdown_to_ast
from markitect.frontmatter import FrontMatterParser
class DocumentManager(CleanDocumentManager):
"""
Document manager for backward compatibility.
This class extends CleanDocumentManager to maintain compatibility
with existing code while using the clean implementation.
"""
def __init__(self, db_manager=None):
super().__init__(db_manager)
def ingest_file(self, file_path: str):
"""
Ingest a markdown file for processing.
This method provides compatibility for tests expecting the ingest_file interface.
"""
import time
import json
from pathlib import Path
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
# Read file content
content = file_path.read_text(encoding='utf-8')
# Extract front matter
start_time = time.time()
parser = FrontMatterParser()
front_matter_data, content_without_front_matter = parser.parse(content)
# Parse to AST
ast = parse_markdown_to_ast(content)
parse_time = time.time() - start_time
# Extract title - first try front matter, then first heading, then filename
title = "Unknown"
if front_matter_data and 'title' in front_matter_data:
title = front_matter_data['title']
elif isinstance(ast, list):
# Look for first H1 heading in AST tokens
for token in ast:
if token.get('type') == 'heading_open' and token.get('tag') == 'h1':
# Find the next inline token with content
idx = ast.index(token) + 1
if idx < len(ast) and ast[idx].get('type') == 'inline':
title = ast[idx].get('content', 'Unknown')
break
# Create actual cache file for compatibility
cache_dir = Path(file_path.parent) / '.ast_cache'
cache_dir.mkdir(exist_ok=True)
cache_file = cache_dir / f"{file_path.stem}_ast.json"
# Write AST to cache file
with open(cache_file, 'w', encoding='utf-8') as f:
json.dump(ast, f, indent=2)
# Store document in database if db_manager exists
if hasattr(self, 'db_manager') and self.db_manager:
try:
# Store using the clean document manager's method
self.store_document(str(file_path), content, ast, front_matter_data)
except Exception:
# If storage fails, continue without error for test compatibility
pass
return {
'ast': ast,
'content': content,
'metadata': {
'filename': file_path.name,
'title': title,
'size': len(content),
'path': str(file_path)
},
'ast_cache_path': cache_file,
'parse_time': parse_time,
'cache_time': 0 # Mock cache time for compatibility
}
# For backward compatibility, also export the clean document manager directly
__all__ = ['DocumentManager', 'CleanDocumentManager']

47
markitect/core/parser.py Normal file
View File

@@ -0,0 +1,47 @@
"""
Markdown AST Parser.
This module provides functionality to parse markdown content into an
Abstract Syntax Tree (AST) using the markdown-it library.
"""
from markdown_it import MarkdownIt
def parse_markdown_to_ast(md_content: str):
"""
Parse markdown content into a JSON-serializable AST.
Args:
md_content: Markdown text to parse
Returns:
List of token dictionaries representing the AST
Example:
ast = parse_markdown_to_ast("# Hello\\n\\nWorld")
"""
# Enable table parsing and other common plugins
md = MarkdownIt("commonmark", {"tables": True}).enable(['table'])
tokens = md.parse(md_content)
# Convert to a JSON-serializable list of dicts
def token_to_dict(token):
d = {
'type': token.type,
'tag': token.tag,
'attrs': token.attrs,
'map': token.map,
'nesting': token.nesting,
'level': token.level,
'children': [token_to_dict(child) if child else None for child in token.children] if token.children else None,
'content': token.content,
'markup': token.markup,
'info': token.info,
'meta': token.meta,
'block': token.block,
'hidden': token.hidden
}
return {k: v for k, v in d.items() if v is not None} # Remove None values
return [token_to_dict(token) for token in tokens]

View File

@@ -0,0 +1,359 @@
"""
AST to Markdown Serialization - Issue #2 Completion
This module provides functionality to serialize markdown-it AST tokens back into
markdown format, enabling roundtrip validation and document manipulation.
Key Features:
- Convert AST tokens back to markdown text
- Preserve front matter during serialization
- Support for content manipulation operations
- Roundtrip integrity validation
"""
from typing import List, Dict, Any, Optional
import yaml
class ASTSerializer:
"""
Serializes markdown-it AST tokens back to markdown format.
Provides roundtrip capability: markdown -> AST -> markdown
Supports front matter preservation and content manipulation.
"""
def __init__(self):
"""Initialize the AST serializer."""
pass
def serialize_to_markdown(self, ast: List[Dict[str, Any]], front_matter: Optional[Dict[str, Any]] = None) -> str:
"""
Convert AST tokens back to markdown format.
Args:
ast: List of markdown-it AST tokens
front_matter: Optional YAML front matter dictionary
Returns:
Markdown text with optional front matter
Example:
serializer = ASTSerializer()
markdown = serializer.serialize_to_markdown(ast, front_matter)
"""
markdown_parts = []
# Add front matter if present
if front_matter and isinstance(front_matter, dict) and front_matter:
yaml_content = yaml.dump(front_matter, default_flow_style=False).strip()
markdown_parts.append(f"---\n{yaml_content}\n---\n\n")
# Process AST tokens
markdown_content = self._process_tokens(ast)
markdown_parts.append(markdown_content)
return ''.join(markdown_parts)
def _process_tokens(self, tokens: List[Dict[str, Any]]) -> str:
"""
Process a list of AST tokens into markdown text.
Args:
tokens: List of markdown-it tokens
Returns:
Markdown text representation
"""
markdown_lines = []
current_line = ""
list_level = 0
for token in tokens:
token_type = token.get('type', '')
content = token.get('content', '')
markup = token.get('markup', '')
tag = token.get('tag', '')
nesting = token.get('nesting', 0)
level = token.get('level', 0)
# Handle different token types
if token_type == 'heading_open':
heading_level = int(tag[1]) if tag.startswith('h') else 1
current_line = '#' * heading_level + ' '
elif token_type == 'heading_close':
if current_line:
markdown_lines.append(current_line.rstrip())
current_line = ""
markdown_lines.append("") # Empty line after heading
elif token_type == 'paragraph_open':
pass # Start of paragraph
elif token_type == 'paragraph_close':
if current_line:
markdown_lines.append(current_line.rstrip())
current_line = ""
markdown_lines.append("") # Empty line after paragraph
elif token_type == 'inline':
# Process inline content and children
if content:
current_line += content
elif 'children' in token:
current_line += self._process_inline_children(token['children'])
elif token_type == 'list_item_open':
# Handle list items
indent = ' ' * (level // 2)
if markup in ('-', '*'):
current_line = indent + '- '
elif markup.isdigit():
current_line = indent + '1. '
elif token_type == 'list_item_close':
if current_line:
markdown_lines.append(current_line.rstrip())
current_line = ""
elif token_type in ('bullet_list_open', 'ordered_list_open'):
list_level += 1
elif token_type in ('bullet_list_close', 'ordered_list_close'):
list_level -= 1
if list_level == 0:
markdown_lines.append("") # Empty line after list
elif token_type == 'blockquote_open':
pass
elif token_type == 'blockquote_close':
markdown_lines.append("")
elif token_type == 'code_block':
markdown_lines.append(f"```{token.get('info', '')}")
markdown_lines.append(content.rstrip())
markdown_lines.append("```")
markdown_lines.append("")
elif token_type == 'fence':
if nesting == 1: # Opening fence
markdown_lines.append(f"```{token.get('info', '')}")
else: # Closing fence
markdown_lines.append("```")
markdown_lines.append("")
elif token_type == 'hr':
markdown_lines.append("---")
markdown_lines.append("")
elif token_type == 'text':
current_line += content
# Add any remaining content
if current_line:
markdown_lines.append(current_line.rstrip())
# Clean up extra empty lines at the end
while markdown_lines and markdown_lines[-1] == "":
markdown_lines.pop()
return '\n'.join(markdown_lines)
def _process_inline_children(self, children: List[Dict[str, Any]]) -> str:
"""
Process inline children tokens (emphasis, strong, links, etc.).
Args:
children: List of inline token children
Returns:
Processed inline markdown text
"""
result = ""
for child in children:
token_type = child.get('type', '')
content = child.get('content', '')
markup = child.get('markup', '')
if token_type == 'text':
result += content
elif token_type == 'code_inline':
result += f"`{content}`"
elif token_type == 'em_open':
result += markup or '*'
elif token_type == 'em_close':
result += markup or '*'
elif token_type == 'strong_open':
result += markup or '**'
elif token_type == 'strong_close':
result += markup or '**'
elif token_type == 'link_open':
# Extract href from attrs
href = ""
if 'attrs' in child and child['attrs']:
for attr in child['attrs']:
if attr[0] == 'href':
href = attr[1]
break
result += "["
elif token_type == 'link_close':
# This is tricky - we need to get the href from the opening token
# For now, we'll use a placeholder approach
result += "](#)"
elif token_type == 'softbreak':
result += '\n'
elif token_type == 'hardbreak':
result += ' \n'
return result
def modify_ast_content(self, ast: List[Dict[str, Any]], modifications: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Modify AST content based on provided modifications.
Args:
ast: Original AST tokens
modifications: Dictionary of modifications to apply
Returns:
Modified AST tokens
Supported modifications:
- add_section: Add a new section with title and content
- update_front_matter: Update front matter values
"""
modified_ast = ast.copy()
# Handle adding sections
if 'add_section' in modifications:
section_data = modifications['add_section']
title = section_data.get('title', 'New Section')
content = section_data.get('content', '')
level = section_data.get('level', 2)
# Create new section tokens
new_tokens = [
{
"type": "heading_open",
"tag": f"h{level}",
"attrs": {},
"map": None,
"nesting": 1,
"level": 0,
"content": "",
"markup": "#" * level,
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "inline",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 0,
"content": title,
"markup": "",
"info": "",
"meta": {},
"block": False,
"hidden": False
}
],
"content": title,
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "heading_close",
"tag": f"h{level}",
"attrs": {},
"map": None,
"nesting": -1,
"level": 0,
"content": "",
"markup": "#" * level,
"info": "",
"meta": {},
"block": True,
"hidden": False
}
]
if content:
new_tokens.extend([
{
"type": "paragraph_open",
"tag": "p",
"attrs": {},
"map": None,
"nesting": 1,
"level": 0,
"content": "",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "inline",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": {},
"map": None,
"nesting": 0,
"level": 0,
"content": content,
"markup": "",
"info": "",
"meta": {},
"block": False,
"hidden": False
}
],
"content": content,
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
},
{
"type": "paragraph_close",
"tag": "p",
"attrs": {},
"map": None,
"nesting": -1,
"level": 0,
"content": "",
"markup": "",
"info": "",
"meta": {},
"block": True,
"hidden": False
}
])
# Add to end of AST
modified_ast.extend(new_tokens)
return modified_ast

475
markitect/core/workspace.py Normal file
View File

@@ -0,0 +1,475 @@
"""
Workspace management functionality for Issue #144.
This module provides workspace templates, multi-project support, and
collaborative workspace features.
"""
import json
import yaml
import shutil
import zipfile
import hashlib
from pathlib import Path
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
from datetime import datetime
from markitect.assets import AssetManager
@dataclass
class TemplateMetadata:
"""Metadata for workspace templates."""
name: str
description: str
version: str
created_at: datetime
asset_count: int
author: str = "Unknown"
tags: List[str] = field(default_factory=list)
@dataclass
class TemplateResult:
"""Result of template creation."""
success: bool
template_path: Path
template_name: str
error: Optional[Exception] = None
@dataclass
class WorkspaceCreationResult:
"""Result of workspace creation from template."""
success: bool
workspace_path: Path
project_name: str
error: Optional[Exception] = None
@dataclass
class ProjectResult:
"""Result of project operations."""
success: bool
project_path: Path
project_name: str
error: Optional[Exception] = None
@dataclass
class SyncResult:
"""Result of workspace synchronization."""
synchronized_count: int
skipped_count: int
error_count: int
errors: List[Exception] = field(default_factory=list)
@dataclass
class BackupResult:
"""Result of workspace backup."""
success: bool
backup_path: Path
backup_size: int
error: Optional[Exception] = None
@dataclass
class RestoreResult:
"""Result of workspace restore."""
success: bool
restored_path: Path
files_restored: int
error: Optional[Exception] = None
@dataclass
class WorkspaceState:
"""Snapshot of workspace state."""
timestamp: datetime
file_checksums: Dict[str, str]
directory_structure: List[str]
asset_hashes: List[str]
@dataclass
class ConflictInfo:
"""Information about a workspace conflict."""
file_path: Path
conflict_type: str
local_timestamp: datetime
remote_timestamp: datetime
@dataclass
class MergeResult:
"""Result of conflict resolution."""
resolved_conflicts: int
unresolved_conflicts: int
merge_strategy: str
class WorkspaceTemplate:
"""Workspace template management."""
def __init__(self, template_path: Path):
"""Initialize workspace template."""
self.template_path = template_path
self.metadata_file = template_path / "template.json"
def get_metadata(self) -> TemplateMetadata:
"""Get template metadata."""
if self.metadata_file.exists():
metadata_dict = json.loads(self.metadata_file.read_text())
return TemplateMetadata(**metadata_dict)
else:
return TemplateMetadata(
name="Unknown",
description="No description",
version="1.0.0",
created_at=datetime.now(),
asset_count=0
)
class WorkspaceManager:
"""Workspace management system."""
def __init__(self, templates_dir: Optional[Path] = None):
"""Initialize workspace manager."""
self.templates_dir = templates_dir or Path.home() / ".markitect" / "templates"
self.templates_dir.mkdir(parents=True, exist_ok=True)
def create_template(self, name: str, source_path: Path, description: str = "",
include_assets: bool = True, configuration: Optional[Dict] = None) -> TemplateResult:
"""Create a workspace template from existing workspace."""
try:
template_path = self.templates_dir / name
template_path.mkdir(exist_ok=True)
# Copy workspace structure
self._copy_workspace_structure(source_path, template_path, include_assets)
# Count assets
asset_count = 0
if include_assets and (source_path / "assets").exists():
asset_count = len(list((source_path / "assets").rglob("*")))
# Create template metadata
metadata = {
"name": name,
"description": description,
"version": "1.0.0",
"created_at": datetime.now().isoformat(),
"asset_count": asset_count,
"author": "Unknown",
"tags": []
}
metadata_file = template_path / "template.json"
metadata_file.write_text(json.dumps(metadata, indent=2))
# Save configuration if provided
if configuration:
config_file = template_path / "markitect.yaml"
config_file.write_text(yaml.dump(configuration, indent=2))
return TemplateResult(
success=True,
template_path=template_path,
template_name=name
)
except Exception as e:
return TemplateResult(
success=False,
template_path=Path(),
template_name=name,
error=e
)
def get_template_metadata(self, template_name: str) -> TemplateMetadata:
"""Get metadata for a specific template."""
template_path = self.templates_dir / template_name
template = WorkspaceTemplate(template_path)
return template.get_metadata()
def create_workspace_from_template(self, template_name: str, target_path: Path,
project_name: str) -> WorkspaceCreationResult:
"""Create a new workspace from a template."""
try:
template_path = self.templates_dir / template_name
if not template_path.exists():
raise FileNotFoundError(f"Template '{template_name}' not found")
# Create target directory
target_path.mkdir(parents=True, exist_ok=True)
# Copy template contents
self._copy_workspace_structure(template_path, target_path, include_assets=True)
# Update project-specific files
self._customize_workspace(target_path, project_name)
return WorkspaceCreationResult(
success=True,
workspace_path=target_path,
project_name=project_name
)
except Exception as e:
return WorkspaceCreationResult(
success=False,
workspace_path=target_path,
project_name=project_name,
error=e
)
def initialize_multi_project_workspace(self, workspace_root: Path):
"""Initialize a multi-project workspace."""
workspace_root.mkdir(parents=True, exist_ok=True)
# Create shared directories
(workspace_root / "shared_assets").mkdir(exist_ok=True)
(workspace_root / "templates").mkdir(exist_ok=True)
(workspace_root / "config").mkdir(exist_ok=True)
# Create workspace configuration
config = {
"workspace_type": "multi_project",
"shared_assets_enabled": True,
"project_isolation": True,
"created_at": datetime.now().isoformat()
}
config_file = workspace_root / "workspace.yaml"
config_file.write_text(yaml.dump(config, indent=2))
def add_project(self, workspace_root: Path, project_name: str,
template: Optional[str] = None) -> ProjectResult:
"""Add a project to multi-project workspace."""
try:
project_path = workspace_root / project_name
project_path.mkdir(exist_ok=True)
if template:
# Use template if specified
result = self.create_workspace_from_template(template, project_path, project_name)
if not result.success:
raise result.error or Exception("Template creation failed")
else:
# Create basic project structure
(project_path / "docs").mkdir(exist_ok=True)
(project_path / "assets").mkdir(exist_ok=True)
return ProjectResult(
success=True,
project_path=project_path,
project_name=project_name
)
except Exception as e:
return ProjectResult(
success=False,
project_path=workspace_root / project_name,
project_name=project_name,
error=e
)
def get_shared_asset_library(self, workspace_root: Path) -> Optional[AssetManager]:
"""Get shared asset library for multi-project workspace."""
shared_assets_path = workspace_root / "shared_assets"
if shared_assets_path.exists():
return AssetManager(storage_path=shared_assets_path)
return None
def initialize_workspace(self, workspace_path: Path):
"""Initialize a single workspace."""
workspace_path.mkdir(parents=True, exist_ok=True)
(workspace_path / "assets").mkdir(exist_ok=True)
(workspace_path / "docs").mkdir(exist_ok=True)
def synchronize_assets(self, source_workspace: Path, target_workspace: Path,
sync_mode: str = "incremental") -> SyncResult:
"""Synchronize assets between workspaces."""
result = SyncResult(
synchronized_count=0,
skipped_count=0,
error_count=0
)
try:
source_assets = source_workspace / "assets"
target_assets = target_workspace / "assets"
if not source_assets.exists():
return result
target_assets.mkdir(exist_ok=True)
# Simple synchronization (copy new files)
for asset_file in source_assets.rglob("*"):
if asset_file.is_file():
relative_path = asset_file.relative_to(source_assets)
target_file = target_assets / relative_path
if not target_file.exists() or sync_mode == "overwrite":
target_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(asset_file, target_file)
result.synchronized_count += 1
else:
result.skipped_count += 1
except Exception as e:
result.error_count += 1
result.errors.append(e)
return result
def create_backup(self, workspace_path: Path, backup_path: Path,
include_assets: bool = True, compression_level: int = 6) -> BackupResult:
"""Create a backup of workspace."""
try:
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=compression_level) as backup_zip:
for file_path in workspace_path.rglob("*"):
if file_path.is_file():
# Skip assets if not included
if not include_assets and "assets" in file_path.parts:
continue
arc_name = file_path.relative_to(workspace_path)
backup_zip.write(file_path, arc_name)
backup_size = backup_path.stat().st_size
return BackupResult(
success=True,
backup_path=backup_path,
backup_size=backup_size
)
except Exception as e:
return BackupResult(
success=False,
backup_path=backup_path,
backup_size=0,
error=e
)
def restore_from_backup(self, backup_path: Path, target_path: Path) -> RestoreResult:
"""Restore workspace from backup."""
try:
target_path.mkdir(parents=True, exist_ok=True)
files_restored = 0
with zipfile.ZipFile(backup_path, 'r') as backup_zip:
backup_zip.extractall(target_path)
files_restored = len(backup_zip.namelist())
return RestoreResult(
success=True,
restored_path=target_path,
files_restored=files_restored
)
except Exception as e:
return RestoreResult(
success=False,
restored_path=target_path,
files_restored=0,
error=e
)
def capture_workspace_state(self, workspace_path: Path) -> WorkspaceState:
"""Capture current state of workspace."""
file_checksums = {}
directory_structure = []
asset_hashes = []
for item_path in workspace_path.rglob("*"):
relative_path = str(item_path.relative_to(workspace_path))
if item_path.is_file():
# Calculate file checksum
content = item_path.read_bytes()
checksum = hashlib.md5(content).hexdigest()
file_checksums[relative_path] = checksum
# Track asset hashes
if "assets" in item_path.parts:
asset_hashes.append(checksum)
directory_structure.append(relative_path)
return WorkspaceState(
timestamp=datetime.now(),
file_checksums=file_checksums,
directory_structure=directory_structure,
asset_hashes=asset_hashes
)
def detect_conflicts(self, state1: WorkspaceState, state2: WorkspaceState) -> List[ConflictInfo]:
"""Detect conflicts between workspace states."""
conflicts = []
# Find files that exist in both states but have different checksums
for file_path, checksum1 in state1.file_checksums.items():
if file_path in state2.file_checksums:
checksum2 = state2.file_checksums[file_path]
if checksum1 != checksum2:
conflict = ConflictInfo(
file_path=Path(file_path),
conflict_type="content_conflict",
local_timestamp=state1.timestamp,
remote_timestamp=state2.timestamp
)
conflicts.append(conflict)
return conflicts
def resolve_conflicts(self, conflicts: List[ConflictInfo],
resolution_strategy: str = "manual") -> MergeResult:
"""Resolve workspace conflicts."""
# Mock conflict resolution
result = MergeResult(
resolved_conflicts=len(conflicts),
unresolved_conflicts=0,
merge_strategy=resolution_strategy
)
return result
def _copy_workspace_structure(self, source: Path, target: Path, include_assets: bool):
"""Copy workspace structure from source to target."""
for item in source.rglob("*"):
if item.is_file():
relative_path = item.relative_to(source)
# Skip assets if not included
if not include_assets and "assets" in relative_path.parts:
continue
# Skip template metadata
if item.name == "template.json":
continue
target_path = target / relative_path
target_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target_path)
def _customize_workspace(self, workspace_path: Path, project_name: str):
"""Customize workspace for specific project."""
# Update any configuration files with project name
config_files = list(workspace_path.glob("*.yaml")) + list(workspace_path.glob("*.yml"))
for config_file in config_files:
try:
content = config_file.read_text()
# Replace placeholder project names
content = content.replace("{{PROJECT_NAME}}", project_name)
content = content.replace("New Project", project_name)
config_file.write_text(content)
except Exception:
pass # Ignore errors in customization