================================================================
RepopackPy Output File
================================================================

This file was generated by RepopackPy on: 2025-04-20T17:11:27.041080

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This header section
2. Repository structure
3. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
1. This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
2. When processing this file, use the separators and "File:" markers to
  distinguish between different files in the repository.
3. Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and RepopackPy's
  configuration.
- Binary files are not included in this packed representation. Please refer to
  the Repository Structure section for a complete list of file paths, including
  binary files.

For more information about RepopackPy, visit: https://github.com/abinthomasonline/repopack-py

================================================================
Repository Structure
================================================================
paelladoc/
  adapters/
    input/
      __init__.py
    output/
      chroma/
        chroma_vector_store_adapter.py
      sqlite/
        models.py
        sqlite_memory_adapter.py
      __init__.py
    persistence/
      __init__.py
    plugins/
      code/
        __init__.py
        code_generation.py
        generate_context.py
        generate_doc.py
      core/
        __init__.py
        continue.py
        help.py
        paella.py
        verification.py
      memory/
        __init__.py
        project_memory.py
      product/
        __init__.py
        product_management.py
      styles/
        __init__.py
        coding_styles.py
        git_workflows.py
      templates/
        __init__.py
        templates.py
      __init__.py
    __init__.py
  application/
    services/
      memory_service.py
      vector_store_service.py
    utils/
      behavior_enforcer.py
    __init__.py
  domain/
    models/
      project.py
    __init__.py
    core_logic.py
  infrastructure/
    __init__.py
  ports/
    input/
      __init__.py
      mcp_port.py
      mcp_server_adapter.py
    output/
      __init__.py
      memory_port.py
      vector_store_port.py
    __init__.py
  __init__.py
tests/
  e2e/
    test_cursor_simulation.py
  integration/
    adapters/
      output/
        test_chroma_vector_store_adapter.py
        test_sqlite_memory_adapter.py
    test_server.py
  unit/
    application/
      services/
        test_memory_service.py
        test_vector_store_service.py
      utils/
        test_behavior_enforcer.py
    domain/
      models/
        test_project.py
    test_ping_tool.py
  README.md

================================================================
Repository Files
================================================================

================
File: paelladoc/adapters/plugins/core/verification.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="core.verification", description="Verifies documentation completeness and consistency.")
def core_verification() -> dict:
    """Checks documentation against templates and project memory.
    
    Calculates an overall quality/completion score.
    Returns an error if documentation is incomplete based on defined criteria.
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for core.verification...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for core.verification"}

================
File: paelladoc/adapters/plugins/core/paella.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging


# Extracted behavior configuration from the original MDC file
BEHAVIOR_CONFIG =     {   'absolute_sequence_enforcement': True,
        'allow_document_improvement': True,
        'allow_native_file_creation': True,
        'always_ask_before_document_creation': True,
        'always_list_options': True,
        'ask_for_next_action': True,
        'autoCreateFolders': True,
        'canCreateFiles': True,
        'canCreateFolders': True,
        'can_create_files': True,
        'can_create_folders': True,
        'confirm_each_parameter': True,
        'conversation_flow': 'paella_initiation_flow',
        'conversation_required': True,
        'copy_templates_to_project': True,
        'createFiles': True,
        'createFolders': True,
        'createMemoryJson': True,
        'createProjectFolder': True,
        'create_files': True,
        'create_folders': True,
        'create_memory_file': True,
        'disallow_external_scripts': True,
        'document_by_document_approach': True,
        'documentation_first': True,
        'enforce_memory_json_creation': True,
        'enforce_one_question_rule': True,
        'enhance_lists_with_emojis': True,
        'fixed_question_order': [   'language',
                                    'project_name',
                                    'project_purpose',
                                    'target_audience',
                                    'project_objectives',
                                    'template_selection'],
        'fixed_question_sequence': True,
        'force_exact_sequence': True,
        'force_single_question_mode': True,
        'guide_through_document_creation': True,
        'interactive': True,
        'iterative_document_creation': True,
        'language_confirmation_first': True,
        'mandatory_language_question_first': True,
        'max_questions_per_message': 1,
        'offer_all_document_templates': True,
        'one_parameter_at_a_time': True,
        'present_document_descriptions': True,
        'prevent_scripts': True,
        'prioritize_document_selection': True,
        'product_documentation_priority': True,
        'prohibit_multiple_questions': True,
        'provide_clear_options': True,
        'require_step_confirmation': True,
        'sequence_language_project_name': True,
        'sequential_questions': True,
        'show_template_menu': True,
        'simplified_initial_questions': True,
        'single_question_mode': True,
        'strict_parameter_sequence': True,
        'strict_question_sequence': True,
        'template_based_documentation': True,
        'track_documentation_completion': True,
        'track_documentation_created': True,
        'update_memory_after_each_document': True,
        'update_templates_with_project_info': True,
        'use_attractive_markdown': True,
        'use_cursor_file_creation': True,
        'use_native_file_creation': True,
        'verify_memory_json_exists': True,
        'wait_for_response': True,
        'wait_for_user_confirmation': True,
        'wait_for_user_response': True}
 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="core.paella", description="Initiates a new PAELLADOC documentation project.")
def core_paella() -> dict:
    """Starts the PAELLADOC documentation process.
    
    Guides the user through project setup questions (language, name, purpose, 
    audience, objectives) and template selection.
    Creates project folder and initial memory file.
    
    Args:
        (No explicit arguments, uses interactive flow based on BEHAVIOR_CONFIG)

    Behavior Config: this tool has associated behavior configuration extracted 
    from the MDC file. See the `BEHAVIOR_CONFIG` variable in the source code.
    """
    
    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for core.paella...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for core.paella"}

================
File: paelladoc/adapters/plugins/core/continue.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging


# Extracted behavior configuration from the original MDC file
BEHAVIOR_CONFIG =     {   'calculate_documentation_completion': True,
        'code_after_documentation': True,
        'confirm_each_parameter': True,
        'conversation_required': True,
        'documentation_first': True,
        'documentation_section_sequence': [   'project_definition',
                                              'market_research',
                                              'user_research',
                                              'problem_definition',
                                              'product_definition',
                                              'architecture_decisions',
                                              'product_roadmap',
                                              'user_stories',
                                              'technical_architecture',
                                              'technical_specifications',
                                              'component_specification',
                                              'api_specification',
                                              'database_design',
                                              'frontend_architecture',
                                              'testing_strategy',
                                              'devops_pipeline',
                                              'security_framework',
                                              'documentation_framework'],
        'enforce_one_question_rule': True,
        'force_single_question_mode': True,
        'guide_documentation_sequence': True,
        'interactive': True,
        'load_memory_file': True,
        'max_questions_per_message': 1,
        'memory_path': '/docs/{project_name}/.memory.json',
        'one_parameter_at_a_time': True,
        'prevent_web_search': True,
        'prohibit_multiple_questions': True,
        'provide_section_guidance': True,
        'require_step_confirmation': True,
        'sequential_questions': True,
        'single_question_mode': True,
        'strict_parameter_sequence': True,
        'strict_question_sequence': True,
        'track_documentation_completion': True,
        'update_last_modified': True,
        'wait_for_response': True,
        'wait_for_user_response': True}
 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="core.continue", description="Guides the user through the documentation sequence based on project memory.")
def core_continue() -> dict:
    """Continues work on an existing documentation project.
    
    Loads project memory, identifies the next step in the defined 
    documentation sequence, and guides the user.
    
    Behavior Config: this tool has associated behavior configuration extracted 
    from the MDC file. See the `BEHAVIOR_CONFIG` variable in the source code.
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for core.continue...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for core.continue"}

================
File: paelladoc/adapters/plugins/core/help.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="core.help", description="Displays help information for PAELLADOC commands.")
def core_help() -> dict:
    """Provides help information for PAELLADOC commands.
    
    Can display general help or help for a specific command.
    This ensures that users can quickly access help without needing 
    to know the specific HELP command syntax.
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for core.help...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for core.help"}

================
File: paelladoc/adapters/plugins/memory/project_memory.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="memory.project_memory", description="Manages the project's memory file (.memory.json)")
def memory_project_memory() -> dict:
    """Handles operations related to the project memory.
    
    Likely used internally by other commands (PAELLA, CONTINUE, VERIFY) 
    to load, save, and update project state, progress, and metadata.
    Provides the HELP CONTEXT (though this might be deprecated).
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for memory.project_memory...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for memory.project_memory"}

================
File: paelladoc/adapters/plugins/code/generate_doc.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="code.generate_doc", description="3. Wait for user selection")
def code_generate_doc() -> dict:
    """3. Wait for user selection"""
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for code.generate_doc...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for code.generate_doc"}

================
File: paelladoc/adapters/plugins/code/generate_context.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="code.generate_context", description="This automatically creates the context file that will be used by GENERATE-DOC for interactive documentation generation.")
def code_generate_context() -> dict:
    """This automatically creates the context file that will be used by GENERATE-DOC for interactive documentation generation."""
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for code.generate_context...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for code.generate_context"}

================
File: paelladoc/adapters/plugins/code/code_generation.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging


# Extracted behavior configuration from the original MDC file
BEHAVIOR_CONFIG =     {   'abort_if_documentation_incomplete': True,
        'code_after_documentation': True,
        'confirm_each_parameter': True,
        'conversation_required': True,
        'documentation_first': True,
        'documentation_verification_path': '/docs/{project_name}/.memory.json',
        'enforce_one_question_rule': True,
        'extract_from_complete_documentation': True,
        'force_single_question_mode': True,
        'guide_to_continue_command': True,
        'interactive': True,
        'max_questions_per_message': 1,
        'one_parameter_at_a_time': True,
        'prevent_web_search': True,
        'prohibit_multiple_questions': True,
        'require_complete_documentation': True,
        'require_step_confirmation': True,
        'required_documentation_sections': [   'project_definition',
                                               'market_research',
                                               'user_research',
                                               'problem_definition',
                                               'product_definition',
                                               'architecture_decisions',
                                               'product_roadmap',
                                               'user_stories',
                                               'technical_architecture',
                                               'technical_specifications',
                                               'api_specification',
                                               'database_design'],
        'sequential_questions': True,
        'single_question_mode': True,
        'strict_parameter_sequence': True,
        'strict_question_sequence': True,
        'verify_documentation_completeness': True,
        'wait_for_response': True,
        'wait_for_user_response': True}
 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="code.code_generation", description="The command uses the script at `.cursor/rules/scripts/extract_repo_content.py` to perform the repository extraction, which leverages repopack-py to convert the codebase to text.")
def code_code_generation() -> dict:
    """The command uses the script at `.cursor/rules/scripts/extract_repo_content.py` to perform the repository extraction, which leverages repopack-py to convert the codebase to text."""
    
    Behavior Config: this tool has associated behavior configuration extracted from the MDC file. See the `BEHAVIOR_CONFIG` variable in the source code.
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for code.code_generation...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for code.code_generation"}

================
File: paelladoc/adapters/plugins/product/product_management.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="product.product_management", description='Manages product features like stories, tasks, etc. Access: stakeholder: ["read_only"]')
def product_product_management() -> dict:
    """Manages product management features.
    
    Handles user stories, tasks, sprints, meeting notes, reports, etc.
    Example access control mentioned in description: stakeholder: ["read_only"]
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for product.product_management...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for product.product_management"}

================
File: paelladoc/adapters/plugins/styles/coding_styles.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="styles.coding_styles", description="Manages coding style guides for the project.")
def styles_coding_styles() -> dict:
    """Applies, customizes, or lists coding styles.
    
    Supports styles like frontend, backend, chrome_extension, etc.
    Uses operations: apply, customize, list, show.
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for styles.coding_styles...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for styles.coding_styles"}

================
File: paelladoc/adapters/plugins/styles/git_workflows.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="styles.git_workflows", description="Manages Git workflow methodologies for the project.")
def styles_git_workflows() -> dict:
    """Applies or customizes Git workflows.
    
    Supports workflows like GitHub Flow, GitFlow, Trunk-Based.
    Provides guidance based on project complexity.
    Simple projects → GitHub Flow
    Complex projects → GitFlow or Trunk-Based
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for styles.git_workflows...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for styles.git_workflows"}

================
File: paelladoc/adapters/plugins/templates/templates.py
================
from mcp.server.fastmcp import mcp
from typing import Optional, List, Dict, Any # Add necessary types
import logging

 # Insert behavior config here

# TODO: Review imports and add any other necessary modules

@mcp.tool(name="templates.templates", description="Manages documentation templates.")
def templates_templates() -> dict:
    """Handles the lifecycle of documentation templates.
    
    Likely allows listing, showing, creating, or updating templates.
    The previous description mentioned workflows, which seems incorrect here.
    """

    # TODO: Implement the actual logic of the command here
    # Access parameters using their variable names (e.g., param)
    # Access behavior config using BEHAVIOR_CONFIG dict (if present)
    logging.info(f"Executing stub for templates.templates...")

    # Example: Print parameters
    local_vars = locals()
    param_values = {  }
    logging.info(f"Parameters received: {param_values}")

    # Replace with actual return value based on command logic
    return {"status": "ok", "message": f"Successfully executed stub for templates.templates"}

================
File: paelladoc/adapters/output/chroma/chroma_vector_store_adapter.py
================
import logging
import uuid
from typing import List, Dict, Any, Optional
from pathlib import Path
import asyncio # For potential async operations if needed

import chromadb
from chromadb.api.models.Collection import Collection
# Import NotFoundError from the appropriate module depending on chromadb version
try:
    from chromadb.errors import NotFoundError
except ImportError:
    try:
        from chromadb.api.errors import NotFoundError
    except ImportError:
        class NotFoundError(ValueError):
            """Fallback NotFoundError inheriting from ValueError for broader compatibility."""
            pass

# Ports and Domain Models/Helpers
from paelladoc.ports.output.vector_store_port import VectorStorePort, SearchResult

logger = logging.getLogger(__name__)

# Default path for persistent ChromaDB data
DEFAULT_CHROMA_PATH = Path.home() / ".paelladoc" / "chroma_data"

class ChromaSearchResult(SearchResult):
    """Concrete implementation of SearchResult for Chroma results."""
    def __init__(self, id: str, distance: Optional[float], metadata: Optional[Dict[str, Any]], document: Optional[str]):
        self.id = id
        self.distance = distance
        self.metadata = metadata
        self.document = document

class ChromaVectorStoreAdapter(VectorStorePort):
    """ChromaDB implementation of the VectorStorePort."""

    def __init__(self, persist_path: Optional[Path] = DEFAULT_CHROMA_PATH, in_memory: bool = False):
        """Initializes the ChromaDB client.
        
        Args:
            persist_path: Path to store persistent Chroma data. Ignored if in_memory is True.
            in_memory: If True, runs ChromaDB entirely in memory (data is lost on exit).
        """
        if in_memory:
            logger.info("Initializing ChromaDB client in-memory.")
            self.client = chromadb.Client()
        else:
            self.persist_path = persist_path or DEFAULT_CHROMA_PATH
            self.persist_path.mkdir(parents=True, exist_ok=True)
            logger.info(f"Initializing persistent ChromaDB client at: {self.persist_path}")
            self.client = chromadb.PersistentClient(path=str(self.persist_path))
            
        # TODO: Consider configuration for embedding function, distance function, etc.
        # Using Chroma's defaults for now (all-MiniLM-L6-v2 and cosine distance)

    async def get_or_create_collection(self, collection_name: str) -> Collection:
        """Gets or creates a Chroma collection."""
        try:
            collection = self.client.get_collection(name=collection_name)
            logger.debug(f"Retrieved existing Chroma collection: {collection_name}")
            return collection
        except (NotFoundError, ValueError) as e:
            # Handle case where collection does not exist (NotFoundError or ValueError)
            if "does not exist" in str(e): # Check if the error indicates non-existence
                 logger.debug(f"Collection '{collection_name}' not found, creating...")
                 collection = self.client.create_collection(name=collection_name)
                 logger.info(f"Created new Chroma collection: {collection_name}")
                 return collection
            else:
                logger.error(f"Unexpected error getting collection '{collection_name}': {e}", exc_info=True)
                raise
        except Exception as e:
             logger.error(f"Error getting or creating collection '{collection_name}': {e}", exc_info=True)
             raise

    async def add_documents(
        self, 
        collection_name: str, 
        documents: List[str], 
        metadatas: Optional[List[Dict[str, Any]]] = None, 
        ids: Optional[List[str]] = None
    ) -> List[str]:
        """Adds documents to the specified Chroma collection."""
        collection = await self.get_or_create_collection(collection_name)
        
        # Generate IDs if not provided
        if not ids:
            ids = [str(uuid.uuid4()) for _ in documents]
        elif len(ids) != len(documents):
            raise ValueError("Number of ids must match number of documents")

        # Add documents to the collection (this handles embedding generation)
        try:
            # collection.add is synchronous in the current chromadb client API
            collection.add(
                documents=documents,
                metadatas=metadatas,
                ids=ids
            )
            logger.info(f"Added {len(documents)} documents to collection '{collection_name}'.")
            return ids
        except Exception as e:
            logger.error(f"Error adding documents to collection '{collection_name}': {e}", exc_info=True)
            raise

    async def search_similar(
        self,
        collection_name: str,
        query_texts: List[str],
        n_results: int = 5,
        where: Optional[Dict[str, Any]] = None,
        where_document: Optional[Dict[str, Any]] = None,
        include: Optional[List[str]] = ["metadatas", "documents", "distances"]
    ) -> List[List[SearchResult]]:
        """Searches for similar documents in the Chroma collection."""
        try:
            collection = self.client.get_collection(name=collection_name)
        except (NotFoundError, ValueError) as e:
             # Handle case where collection does not exist
             if "does not exist" in str(e):
                 logger.warning(f"Collection '{collection_name}' not found for search.")
                 return [[] for _ in query_texts]
             else:
                logger.error(f"Unexpected error retrieving collection '{collection_name}' for search: {e}", exc_info=True)
                raise
        except Exception as e:
             logger.error(f"Error retrieving collection '{collection_name}' for search: {e}", exc_info=True)
             raise

        try:
            # collection.query is synchronous
            results = collection.query(
                query_texts=query_texts,
                n_results=n_results,
                where=where,
                where_document=where_document,
                include=include
            )
            
            # Map Chroma's result structure to our SearchResult list of lists
            # Chroma returns a dict with keys like 'ids', 'distances', 'metadatas', 'documents'
            # Each value is a list of lists (one inner list per query)
            mapped_results: List[List[SearchResult]] = []
            num_queries = len(query_texts)
            result_ids = results.get('ids') or [[] for _ in range(num_queries)]
            result_distances = results.get('distances') or [[] for _ in range(num_queries)]
            result_metadatas = results.get('metadatas') or [[] for _ in range(num_queries)]
            result_documents = results.get('documents') or [[] for _ in range(num_queries)]

            for i in range(num_queries):
                query_results = []
                # Ensure all result lists have the expected length for the i-th query
                num_docs_for_query = len(result_ids[i]) if result_ids and i < len(result_ids) else 0
                for j in range(num_docs_for_query):
                     query_results.append(ChromaSearchResult(
                         id=result_ids[i][j] if result_ids and i < len(result_ids) and j < len(result_ids[i]) else "N/A",
                         distance=result_distances[i][j] if result_distances and i < len(result_distances) and j < len(result_distances[i]) else None,
                         metadata=result_metadatas[i][j] if result_metadatas and i < len(result_metadatas) and j < len(result_metadatas[i]) else None,
                         document=result_documents[i][j] if result_documents and i < len(result_documents) and j < len(result_documents[i]) else None
                     ))
                mapped_results.append(query_results)
                
            return mapped_results
            
        except Exception as e:
            logger.error(f"Error querying collection '{collection_name}': {e}", exc_info=True)
            raise
            
    async def delete_collection(self, collection_name: str) -> None:
        """Deletes a Chroma collection."""
        try:
            self.client.delete_collection(name=collection_name)
            logger.info(f"Deleted Chroma collection: {collection_name}")
        except (NotFoundError, ValueError) as e:
             # Handle case where collection does not exist
             if "does not exist" in str(e):
                 logger.warning(f"Attempted to delete non-existent collection: {collection_name}")
             else:
                logger.error(f"Unexpected error deleting collection '{collection_name}': {e}", exc_info=True)
                raise
        except Exception as e:
            logger.error(f"Error deleting collection '{collection_name}': {e}", exc_info=True)
            raise

================
File: paelladoc/adapters/output/sqlite/models.py
================
from typing import List, Optional, Dict, Any
from sqlmodel import Field, Relationship, SQLModel, Column, JSON
import datetime

# Note: Domain Enums like DocumentStatus are not directly used here,
# we store their string representation (e.g., 'pending').
# The adapter layer will handle the conversion.

# --- Database Models --- 

# Forward references are needed for relationships defined before the target model

class ProjectMetadataDB(SQLModel, table=True):
    # Represents the metadata associated with a project memory entry
    id: Optional[int] = Field(default=None, primary_key=True)
    # name field is stored in ProjectMemoryDB as it's the primary identifier
    language: Optional[str] = None
    purpose: Optional[str] = None
    target_audience: Optional[str] = None
    objectives: Optional[List[str]] = Field(default=None, sa_column=Column(JSON))

    # Define the one-to-one relationship back to ProjectMemoryDB
    # Use Optional because a metadata row might briefly exist before being linked
    project_memory: Optional["ProjectMemoryDB"] = Relationship(back_populates="project_meta")

class ProjectDocumentDB(SQLModel, table=True):
    # Represents a single document tracked within a project memory
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True) # Name of the document file (e.g., "README.md")
    template_origin: Optional[str] = None
    status: str = Field(default="pending", index=True) # Store enum string value

    # Foreign key to link back to the main project memory entry
    project_memory_id: Optional[int] = Field(default=None, foreign_key="projectmemorydb.id")
    # Define the many-to-one relationship back to ProjectMemoryDB
    project_memory: Optional["ProjectMemoryDB"] = Relationship(back_populates="documents")

class ProjectMemoryDB(SQLModel, table=True):
    # Represents the main project memory entry in the database
    id: Optional[int] = Field(default=None, primary_key=True)
    # Use project_name from metadata as the main unique identifier for lookups
    project_name: str = Field(index=True, unique=True)
    
    created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
    last_updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)

    # Foreign key to link to the associated metadata entry
    project_meta_id: Optional[int] = Field(default=None, foreign_key="projectmetadatadb.id", unique=True)
    # Define the one-to-one relationship to ProjectMetadataDB
    project_meta: Optional[ProjectMetadataDB] = Relationship(back_populates="project_memory")

    # Define the one-to-many relationship to ProjectDocumentDB
    documents: List[ProjectDocumentDB] = Relationship(back_populates="project_memory")

================
File: paelladoc/adapters/output/sqlite/sqlite_memory_adapter.py
================
import logging
from typing import Optional, Dict
from pathlib import Path
import datetime

from sqlmodel import SQLModel, Session, select
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, selectinload
from sqlalchemy.exc import NoResultFound, IntegrityError

# Ports and Domain Models
from paelladoc.ports.output.memory_port import MemoryPort
from paelladoc.domain.models.project import ProjectMemory, ProjectMetadata, ProjectDocument, DocumentStatus

# Database Models for this adapter
from .models import ProjectMemoryDB, ProjectMetadataDB, ProjectDocumentDB

logger = logging.getLogger(__name__)

# Default path for the SQLite database file
DEFAULT_DB_PATH = Path.home() / ".paelladoc" / "memory.db"

class SQLiteMemoryAdapter(MemoryPort):
    """SQLite implementation of the MemoryPort for project persistence."""

    def __init__(self, db_path: Optional[Path] = None):
        self.db_path = db_path or DEFAULT_DB_PATH
        self.db_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
        self.engine_url = f"sqlite+aiosqlite:///{self.db_path.resolve()}" # Use aiosqlite for async
        self.async_engine = create_async_engine(self.engine_url, echo=False) # Set echo=True for debugging SQL
        # Async session factory
        self.AsyncSessionFactory = sessionmaker(
            bind=self.async_engine, class_=AsyncSession, expire_on_commit=False
        )
        logger.info(f"SQLiteMemoryAdapter initialized with database at: {self.db_path}")
        # We might want to trigger table creation asynchronously upon first use or during init
        # For simplicity now, let's assume tables exist or handle creation within methods

    async def _create_db_and_tables(self):
        """Creates the database and tables if they don't exist."""
        async with self.async_engine.begin() as conn:
            # This reflects table metadata and creates tables if they don't exist.
            # It uses SQLAlchemy's underlying mechanism.
            await conn.run_sync(SQLModel.metadata.create_all)
        logger.info("Database tables checked/created.")

    # --- Helper for mapping --- #
    def _map_db_to_domain(self, db_memory: ProjectMemoryDB) -> ProjectMemory:
        """Maps the DB model hierarchy to the domain ProjectMemory model."""
        if not db_memory.project_meta:
            logger.error(f"Inconsistent data: ProjectMemoryDB {db_memory.id} is missing project_meta.")
            raise ValueError(f"Missing metadata for project: {db_memory.project_name}")
        
        domain_metadata = ProjectMetadata(
            name=db_memory.project_name, 
            language=db_memory.project_meta.language,
            purpose=db_memory.project_meta.purpose,
            target_audience=db_memory.project_meta.target_audience,
            objectives=db_memory.project_meta.objectives 
        )
        
        domain_documents = {}
        for db_doc in db_memory.documents:
            domain_documents[db_doc.name] = ProjectDocument(
                name=db_doc.name,
                template_origin=db_doc.template_origin,
                status=DocumentStatus(db_doc.status) 
            )
            
        domain_memory = ProjectMemory(
            metadata=domain_metadata,
            documents=domain_documents,
            created_at=db_memory.created_at,
            last_updated_at=db_memory.last_updated_at
        )
        return domain_memory

    # --- MemoryPort Implementation --- #

    async def save_memory(self, memory: ProjectMemory) -> None:
        """Saves the project memory state to the SQLite database.
           Handles both creation of new projects and updates to existing ones.
        """
        logger.debug(f"Attempting to save memory for project: {memory.metadata.name}")
        await self._create_db_and_tables()

        async with self.AsyncSessionFactory() as session:
            try:
                statement = select(ProjectMemoryDB).where(ProjectMemoryDB.project_name == memory.metadata.name)\
                           .options(selectinload(ProjectMemoryDB.project_meta), 
                                    selectinload(ProjectMemoryDB.documents))
                results = await session.execute(statement)
                existing_db_memory = results.scalars().first()

                if existing_db_memory:
                    # --- Update Existing Project --- 
                    logger.debug(f"Project '{memory.metadata.name}' found. Updating...")
                    
                    existing_db_memory.last_updated_at = datetime.datetime.now()
                    memory.update_timestamp() 
                    
                    # Update Metadata (using project_meta)
                    if existing_db_memory.project_meta:
                        existing_db_memory.project_meta.language = memory.metadata.language
                        existing_db_memory.project_meta.purpose = memory.metadata.purpose
                        existing_db_memory.project_meta.target_audience = memory.metadata.target_audience
                        existing_db_memory.project_meta.objectives = memory.metadata.objectives
                        session.add(existing_db_memory.project_meta)
                    else:
                         logger.error(f"Data inconsistency: Existing ProjectMemoryDB {existing_db_memory.id} has no project_meta link.")

                    # Update Documents (logic remains the same, refers to existing_db_memory.documents)
                    existing_docs_map: Dict[str, ProjectDocumentDB] = {doc.name: doc for doc in existing_db_memory.documents}
                    
                    for domain_doc_name, domain_doc in memory.documents.items():
                        existing_db_doc = existing_docs_map.get(domain_doc_name)
                        if existing_db_doc:
                            existing_db_doc.status = domain_doc.status.value
                            existing_db_doc.template_origin = domain_doc.template_origin
                            session.add(existing_db_doc)
                        else:
                            new_db_doc = ProjectDocumentDB(
                                name=domain_doc.name,
                                template_origin=domain_doc.template_origin,
                                status=domain_doc.status.value,
                                project_memory_id=existing_db_memory.id 
                            )
                            session.add(new_db_doc)
                            existing_db_memory.documents.append(new_db_doc) 
                            
                    session.add(existing_db_memory) 

                else:
                    # --- Create New Project --- 
                    logger.debug(f"Project '{memory.metadata.name}' not found. Creating new entry...")
                    
                    # Map domain models to DB models (using project_meta)
                    new_db_metadata = ProjectMetadataDB(
                        language=memory.metadata.language,
                        purpose=memory.metadata.purpose,
                        target_audience=memory.metadata.target_audience,
                        objectives=memory.metadata.objectives
                    )
                    
                    new_db_memory = ProjectMemoryDB(
                        project_name=memory.metadata.name,
                        created_at=memory.created_at,
                        last_updated_at=memory.last_updated_at,
                        project_meta=new_db_metadata # Link relationship (renamed)
                    )
                    
                    new_db_docs = []
                    for domain_doc in memory.documents.values():
                        new_db_doc = ProjectDocumentDB(
                            name=domain_doc.name,
                            template_origin=domain_doc.template_origin,
                            status=domain_doc.status.value,
                            project_memory=new_db_memory # Link relationship back
                        )
                        new_db_docs.append(new_db_doc)
                        
                    new_db_memory.documents = new_db_docs 
                    
                    session.add(new_db_memory)
                
                await session.commit()
                logger.info(f"Successfully saved memory for project: {memory.metadata.name}")

            except IntegrityError as e:
                await session.rollback() 
                logger.error(f"Integrity error saving project '{memory.metadata.name}': {e}", exc_info=True)
                raise
            except Exception as e:
                await session.rollback()
                logger.error(f"Unexpected error saving project '{memory.metadata.name}': {e}", exc_info=True)
                raise

    async def load_memory(self, project_name: str) -> Optional[ProjectMemory]:
        """Loads project memory from the SQLite database."""
        logger.debug(f"Attempting to load memory for project: {project_name}")
        await self._create_db_and_tables() 

        async with self.AsyncSessionFactory() as session:
            try:
                statement = select(ProjectMemoryDB).where(ProjectMemoryDB.project_name == project_name)\
                           .options(selectinload(ProjectMemoryDB.project_meta), 
                                    selectinload(ProjectMemoryDB.documents))
                
                results = await session.execute(statement)
                db_memory = results.scalars().first()
                
                if db_memory:
                    logger.debug(f"Found project '{project_name}' in DB, mapping to domain model.")
                    return self._map_db_to_domain(db_memory)
                else:
                    logger.debug(f"Project '{project_name}' not found in DB.")
                    return None
            except Exception as e:
                logger.error(f"Error loading project '{project_name}': {e}", exc_info=True)
                return None 

    async def project_exists(self, project_name: str) -> bool:
        """Checks if a project memory exists in the SQLite database."""
        logger.debug(f"Checking existence for project: {project_name}")
        # Ensure tables exist before trying to check
        await self._create_db_and_tables() 

        async with self.AsyncSessionFactory() as session:
            try:
                statement = select(ProjectMemoryDB).where(ProjectMemoryDB.project_name == project_name)
                results = await session.execute(statement)
                existing_project = results.scalars().first()
                exists = existing_project is not None
                logger.debug(f"Project '{project_name}' exists: {exists}")
                return exists
            except Exception as e:
                logger.error(f"Error checking project existence for '{project_name}': {e}", exc_info=True)
                return False # Or re-raise depending on desired error handling

================
File: paelladoc/application/utils/behavior_enforcer.py
================
"""
Utility for enforcing behavior rules defined in tool configurations.
"""

import logging
from typing import Dict, Any, Set, Optional

# Assuming MCPContext structure or relevant parts are accessible
# from mcp.context import Context as MCPContext # Or use Any for now

logger = logging.getLogger(__name__)

class BehaviorViolationError(Exception):
    """Custom exception raised when a behavior rule is violated."""
    def __init__(self, message: str):
        self.message = message
        super().__init__(self.message)

class BehaviorEnforcer:
    """Enforces conversational behavior based on tool config and context."""

    @staticmethod
    def enforce(
        tool_name: str,
        behavior_config: Optional[Dict[str, Any]], 
        ctx: Optional[Any], # Replace Any with actual MCPContext type if available
        provided_args: Optional[Dict[str, Any]]
    ):
        """Checks current context and arguments against behavior rules.
        
        Args:
            tool_name: The name of the tool being called.
            behavior_config: The BEHAVIOR_CONFIG dictionary for the tool.
            ctx: The current MCP context object (expected to have ctx.progress).
            provided_args: The arguments passed to the tool function in the current call.
            
        Raises:
            BehaviorViolationError: If a rule is violated.
        """
        if not behavior_config:
            logger.debug(f"No behavior config for tool '{tool_name}', skipping enforcement.")
            return
        
        if not ctx or not hasattr(ctx, 'progress') or not provided_args:
            logger.warning(f"Behavior enforcement skipped for '{tool_name}': missing context or args.")
            # Decide if this should be an error or just skipped
            return 

        # --- Enforce fixed_question_order --- 
        if "fixed_question_order" in behavior_config:
            sequence = behavior_config["fixed_question_order"]
            if not isinstance(sequence, list):
                 logger.warning(f"Invalid 'fixed_question_order' in config for {tool_name}. Skipping check.")
                 return

            # Assume ctx.progress['collected_params'] holds previously gathered arguments
            collected_params: Set[str] = ctx.progress.get("collected_params", set())
            
            # Identify arguments provided in *this* specific call (non-None values)
            current_call_args = {k for k, v in provided_args.items() if v is not None}
            
            # Identify which of the currently provided args are *new* (not already collected)
            newly_provided_params = current_call_args - collected_params

            if not newly_provided_params:
                # No *new* parameters were provided in this call. 
                # This might be okay if just confirming or if sequence is done.
                # Or maybe it should error if the sequence is *not* done?
                # For now, allow proceeding. Behavior could be refined.
                logger.debug(f"Tool '{tool_name}': No new parameters provided, sequence check passes by default.")
                return

            # Find the first parameter in the defined sequence that hasn't been collected yet
            expected_next_param = None
            for param in sequence:
                if param not in collected_params:
                    expected_next_param = param
                    break

            if expected_next_param is None:
                # The defined sequence is complete.
                # Should we allow providing *other* (optional?) parameters now?
                # If strict_parameter_sequence is True, maybe disallow?
                # For now, allow extra parameters after the main sequence.
                logger.debug(f"Tool '{tool_name}': Sequence complete, allowing provided args: {newly_provided_params}")
                return

            # --- Enforce one_parameter_at_a_time (implicitly for sequence) --- 
            # Check if exactly one *new* parameter was provided and if it's the expected one.
            if len(newly_provided_params) > 1:
                 raise BehaviorViolationError(
                     f"Tool '{tool_name}' expects parameters sequentially. "
                     f"Expected next: '{expected_next_param}'. "
                     f"Provided multiple new parameters: {newly_provided_params}. "
                     f"Collected so far: {collected_params}."
                 )
                 
            provided_param = list(newly_provided_params)[0]
            if provided_param != expected_next_param:
                 raise BehaviorViolationError(
                     f"Tool '{tool_name}' expects parameters sequentially. "
                     f"Expected next: '{expected_next_param}'. "
                     f"Got unexpected new parameter: '{provided_param}'. "
                     f"Collected so far: {collected_params}."
                 )

            # If we reach here, exactly one new parameter was provided and it was the expected one.
            logger.debug(f"Tool '{tool_name}': Correct sequential parameter '{provided_param}' provided.")
            
        # --- Add other rule checks here as needed --- 
        # e.g., max_questions_per_message (more complex, needs turn context)
        # e.g., documentation_first (likely better as separate middleware/check)

        # If all checks pass
        return

================
File: paelladoc/application/services/vector_store_service.py
================
import logging
from typing import List, Dict, Any, Optional

# Ports and SearchResult
from paelladoc.ports.output.vector_store_port import VectorStorePort, SearchResult

logger = logging.getLogger(__name__)

class VectorStoreService:
    """Application service for interacting with the vector store.
    
    Uses the VectorStorePort to abstract the underlying vector database.
    """

    def __init__(self, vector_store_port: VectorStorePort):
        """Initializes the service with a VectorStorePort implementation."""
        self.vector_store_port = vector_store_port
        logger.info(f"VectorStoreService initialized with port: {type(vector_store_port).__name__}")

    async def add_texts_to_collection(
        self,
        collection_name: str,
        documents: List[str],
        metadatas: Optional[List[Dict[str, Any]]] = None,
        ids: Optional[List[str]] = None
    ) -> List[str]:
        """Adds text documents to a specific collection."""
        logger.debug(f"Service: Adding {len(documents)} documents to vector store collection '{collection_name}'")
        try:
            added_ids = await self.vector_store_port.add_documents(
                collection_name=collection_name,
                documents=documents,
                metadatas=metadatas,
                ids=ids
            )
            logger.info(f"Service: Successfully added documents to collection '{collection_name}' with IDs: {added_ids}")
            return added_ids
        except Exception as e:
            logger.error(f"Service: Error adding documents to collection '{collection_name}': {e}", exc_info=True)
            # Re-raise or handle specific exceptions as needed
            raise

    async def find_similar_texts(
        self,
        collection_name: str,
        query_texts: List[str],
        n_results: int = 5,
        filter_metadata: Optional[Dict[str, Any]] = None,
        filter_document: Optional[Dict[str, Any]] = None
    ) -> List[List[SearchResult]]:
        """Finds documents similar to the query texts within a collection."""
        logger.debug(f"Service: Searching collection '{collection_name}' for texts similar to: {query_texts} (n={n_results})")
        try:
            results = await self.vector_store_port.search_similar(
                collection_name=collection_name,
                query_texts=query_texts,
                n_results=n_results,
                where=filter_metadata, # Pass filters to the port
                where_document=filter_document,
                # Include common fields by default
                include=["metadatas", "documents", "distances", "ids"] 
            )
            logger.info(f"Service: Found {sum(len(r) for r in results)} potential results for {len(query_texts)} queries in '{collection_name}'.")
            return results
        except Exception as e:
            logger.error(f"Service: Error searching collection '{collection_name}': {e}", exc_info=True)
            # Re-raise or handle specific exceptions as needed
            raise

    async def ensure_collection_exists(self, collection_name: str):
        """Ensures a collection exists, creating it if necessary."""
        logger.debug(f"Service: Ensuring collection '{collection_name}' exists.")
        try:
            await self.vector_store_port.get_or_create_collection(collection_name)
            logger.info(f"Service: Collection '{collection_name}' checked/created.")
        except Exception as e:
            logger.error(f"Service: Error ensuring collection '{collection_name}' exists: {e}", exc_info=True)
            raise
            
    async def remove_collection(self, collection_name: str):
        """Removes a collection entirely."""
        logger.debug(f"Service: Attempting to remove collection '{collection_name}'.")
        try:
            await self.vector_store_port.delete_collection(collection_name)
            logger.info(f"Service: Collection '{collection_name}' removed.")
        except Exception as e:
            logger.error(f"Service: Error removing collection '{collection_name}': {e}", exc_info=True)
            raise

================
File: paelladoc/application/services/memory_service.py
================
import logging
from typing import Optional

# Domain Models
from paelladoc.domain.models.project import ProjectMemory, ProjectDocument, DocumentStatus

# Ports
from paelladoc.ports.output.memory_port import MemoryPort

logger = logging.getLogger(__name__)

class MemoryService:
    """Application service for managing project memory operations.
    
    Uses the MemoryPort to interact with the persistence layer.
    """

    def __init__(self, memory_port: MemoryPort):
        """Initializes the service with a MemoryPort implementation."""
        self.memory_port = memory_port
        logger.info(f"MemoryService initialized with port: {type(memory_port).__name__}")

    async def get_project_memory(self, project_name: str) -> Optional[ProjectMemory]:
        """Retrieves the memory for a specific project."""
        logger.debug(f"Service: Attempting to get memory for project '{project_name}'")
        return await self.memory_port.load_memory(project_name)

    async def check_project_exists(self, project_name: str) -> bool:
        """Checks if a project memory already exists."""
        logger.debug(f"Service: Checking existence for project '{project_name}'")
        return await self.memory_port.project_exists(project_name)

    async def create_project_memory(self, memory: ProjectMemory) -> ProjectMemory:
        """Creates a new project memory entry.
        
        Raises:
            ValueError: If a project with the same name already exists.
        """
        project_name = memory.metadata.name
        logger.debug(f"Service: Attempting to create memory for project '{project_name}'")
        
        exists = await self.check_project_exists(project_name)
        if exists:
            logger.error(f"Cannot create project '{project_name}': already exists.")
            raise ValueError(f"Project memory for '{project_name}' already exists.")
            
        await self.memory_port.save_memory(memory)
        logger.info(f"Service: Successfully created memory for project '{project_name}'")
        return memory # Return the saved object (could also reload it)

    async def update_project_memory(self, memory: ProjectMemory) -> ProjectMemory:
        """Updates an existing project memory entry.
        
        Raises:
            ValueError: If the project does not exist.
        """
        project_name = memory.metadata.name
        logger.debug(f"Service: Attempting to update memory for project '{project_name}'")
        
        # Ensure the project exists before attempting an update
        # Note: save_memory itself handles the create/update logic, but this check
        # makes the service layer's intent clearer and prevents accidental creation.
        exists = await self.check_project_exists(project_name)
        if not exists:
            logger.error(f"Cannot update project '{project_name}': does not exist.")
            raise ValueError(f"Project memory for '{project_name}' does not exist. Use create_project_memory first.")

        await self.memory_port.save_memory(memory)
        logger.info(f"Service: Successfully updated memory for project '{project_name}'")
        return memory # Return the updated object

    # Example of a more specific use case method:
    async def update_document_status_in_memory(
        self, project_name: str, document_name: str, new_status: DocumentStatus
    ) -> Optional[ProjectMemory]:
        """Updates the status of a specific document within a project's memory."""
        logger.debug(f"Service: Updating status for document '{document_name}' in project '{project_name}' to {new_status}")
        memory = await self.get_project_memory(project_name)
        if not memory:
            logger.warning(f"Project '{project_name}' not found, cannot update document status.")
            return None
        
        if document_name not in memory.documents:
             logger.warning(f"Document '{document_name}' not found in project '{project_name}', cannot update status.")
             # Or should we raise an error?
             return memory # Return unchanged memory?
        
        memory.update_document_status(document_name, new_status) # Use domain model method
        
        # Save the updated memory
        await self.memory_port.save_memory(memory)
        logger.info(f"Service: Saved updated status for document '{document_name}' in project '{project_name}'")
        return memory

================
File: paelladoc/ports/input/mcp_port.py
================
from abc import ABC, abstractmethod
from typing import Any, Dict

class MCPPort(ABC):
    """Input port for MCP (Model-Command-Process) operations."""
    
    @abstractmethod
    def process_command(self, command: str, args: Dict[str, Any]) -> Dict[str, Any]:
        """Process an MCP command with its arguments."""
        pass
    
    @abstractmethod
    def register_plugin(self, plugin: Any) -> None:
        """Register a new plugin."""
        pass

================
File: paelladoc/ports/input/mcp_server_adapter.py
================
#!/usr/bin/env python3
"""
PAELLADOC MCP Server entry point (Input Adapter).

Relies on paelladoc_core.py (now core_logic.py in domain) for MCP functionality and FastMCP instance.
Simply runs the imported MCP instance.
Adds server-specific resources and prompts using decorators.
"""

import sys
import logging
from pathlib import Path
from typing import Optional, Dict, Any
import time # Add time import

# Import TextContent for prompt definition
from mcp.types import TextContent # Assuming mcp is installed in .venv

# Import the core FastMCP instance and logger from the domain layer
from paelladoc.domain.core_logic import mcp, logger # Corrected import path

# --- Add specific tools/resources/prompts for this entry point using decorators --- #

@mcp.resource("docs://readme") # Use decorator
def get_readme() -> str:
    """Get the project README content."""
    try:
        # Assuming README.md is in the project root (cwd)
        readme_path = Path("README.md")
        if readme_path.exists():
            return readme_path.read_text()
        else:
            logger.warning("README.md not found in project root.")
            return "README.md not found" # Keep simple return for resource
    except Exception as e:
        logger.error(f"Error reading README.md: {e}", exc_info=True)
        return f"Error reading README.md: {str(e)}"

@mcp.resource("docs://templates/{template_name}") # Use decorator
def get_template(template_name: str) -> str:
    """Get a documentation template."""
    # Corrected path relative to src directory
    base_path = Path(__file__).parent.parent.parent.parent # Should point to src/
    template_path = base_path / "paelladoc" / "adapters" / "plugins" / "templates" / f"{template_name}.md"
    try:
        if template_path.exists():
            return template_path.read_text()
        else:
            logger.warning(f"Template {template_name} not found at {template_path}")
            return f"Error: Template {template_name} not found"
    except Exception as e:
        logger.error(f"Error reading template {template_name}: {e}", exc_info=True)
        return f"Error reading template {template_name}: {str(e)}"

@mcp.prompt() # Use decorator
def paella_command(project_name: str) -> TextContent:
    """Create a PAELLA command prompt."""
    return TextContent(
        type="text",
        text=f"Initiating PAELLADOC for project: {project_name}.\n" 
             f"Please specify: 1. Project type, 2. Methodologies, 3. Git workflow."
    )

# --- Main Execution Logic --- #

if __name__ == "__main__":
    # Configure file logging
    try:
        log_file = 'paelladoc_server.log' 
        file_handler = logging.FileHandler(log_file)
        file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
        logging.getLogger().addHandler(file_handler)
        logging.getLogger().setLevel(logging.DEBUG)
        logger.info(f"Logging configured. Outputting to {log_file}")
    except Exception as log_e:
        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
        # Re-get logger after basicConfig potentially reconfigured root
        logger = logging.getLogger(__name__) 
        logger.error(f"Could not configure file logging: {log_e}. Logging to stderr.")

    # Check command line arguments to determine run mode
    run_mode = "stdio" if "--stdio" in sys.argv else "web" # Default to stdio if --stdio present

    try:
        if run_mode == "stdio":
            logger.info("Starting PAELLADOC MCP server in STDIO mode via FastMCP mcp.run(transport='stdio')...")
            logger.debug("Waiting 10 seconds before mcp.run()...")
            time.sleep(10) # Add sleep before run
            logger.debug("Attempting mcp.run(transport=\"stdio\")")
            mcp.run(transport="stdio") # Explicitly request stdio transport
        else:
            # Attempt to run the default web server (SSE)
            # Note: FastMCP's default run() might try stdio first anyway if no host/port specified
            logger.warning("Starting PAELLADOC MCP server in default mode (likely web/SSE) via FastMCP mcp.run()...")
            logger.warning("Use --stdio argument for direct client integration.")
            mcp.run() # Run with default settings (tries SSE/web)

        logger.info(f"PAELLADOC MCP server finished (mode: {run_mode}).") 
    except Exception as e:
        logger.critical(f"Failed to start or run MCP server: {e}", exc_info=True)
        sys.exit(1)

================
File: paelladoc/ports/output/vector_store_port.py
================
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional

class SearchResult(ABC):
    """Represents a single search result from the vector store."""
    # Define common attributes for a search result
    id: str
    distance: Optional[float] = None
    metadata: Optional[Dict[str, Any]] = None
    document: Optional[str] = None

class VectorStorePort(ABC):
    """Output Port defining operations for a vector store."""

    @abstractmethod
    async def add_documents(
        self, 
        collection_name: str, 
        documents: List[str], 
        metadatas: Optional[List[Dict[str, Any]]] = None, 
        ids: Optional[List[str]] = None
    ) -> List[str]:
        """Adds documents (text) to a specific collection in the vector store.
        
        Embeddings are typically generated automatically by the implementation.

        Args:
            collection_name: The name of the collection to add documents to.
            documents: A list of text documents to add.
            metadatas: Optional list of metadata dictionaries corresponding to each document.
            ids: Optional list of unique IDs for each document.

        Returns:
            A list of IDs for the added documents.
        """
        pass

    @abstractmethod
    async def search_similar(
        self,
        collection_name: str,
        query_texts: List[str],
        n_results: int = 5,
        where: Optional[Dict[str, Any]] = None,
        where_document: Optional[Dict[str, Any]] = None,
        include: Optional[List[str]] = ["metadatas", "documents", "distances"]
    ) -> List[List[SearchResult]]:
        """Searches for documents in a collection similar to the query texts.

        Args:
            collection_name: The name of the collection to search within.
            query_texts: A list of query texts to find similar documents for.
            n_results: The maximum number of results to return for each query.
            where: Optional filter criteria for metadata.
            where_document: Optional filter criteria for document content.
            include: Optional list specifying what data to include in results.

        Returns:
            A list of lists of SearchResult objects, one list per query text.
        """
        pass
        
    @abstractmethod
    async def get_or_create_collection(self, collection_name: str) -> Any:
        """Gets or creates a collection in the vector store.
        
        The return type is Any for now, as it depends on the specific library's
        collection object representation (e.g., Chroma's Collection).
        
        Args:
            collection_name: The name of the collection.
            
        Returns:
            The collection object.
        """
        pass

    @abstractmethod
    async def delete_collection(self, collection_name: str) -> None:
        """Deletes a collection from the vector store.
        
        Args:
            collection_name: The name of the collection to delete.
        """
        pass

    # Add other potential methods like:
    # async def delete_documents(self, collection_name: str, ids: List[str]) -> None: ...
    # async def update_documents(...) -> None: ...

================
File: paelladoc/ports/output/memory_port.py
================
from abc import ABC, abstractmethod
from typing import Optional

# Import the domain model it needs to interact with
from paelladoc.domain.models.project import ProjectMemory

class MemoryPort(ABC):
    """Output Port defining operations for project memory persistence."""

    @abstractmethod
    async def save_memory(self, memory: ProjectMemory) -> None:
        """Saves the entire project memory state.
        
        Args:
            memory: The ProjectMemory object to save.
        """
        pass

    @abstractmethod
    async def load_memory(self, project_name: str) -> Optional[ProjectMemory]:
        """Loads the project memory state for a given project name.
        
        Args:
            project_name: The unique name of the project to load.
            
        Returns:
            The ProjectMemory object if found, otherwise None.
        """
        pass

    @abstractmethod
    async def project_exists(self, project_name: str) -> bool:
        """Checks if a project memory exists for the given name.
        
        Args:
            project_name: The unique name of the project to check.
            
        Returns:
            True if the project memory exists, False otherwise.
        """
        pass

    # Potentially add other methods later if needed, e.g., delete_memory

================
File: paelladoc/domain/core_logic.py
================
"""
Core PAELLADOC MCP Logic.

Handles MCP instance creation, plugin loading, and base tool registration.
Uses FastMCP for compatibility with decorators.
"""
import logging
from mcp.server.fastmcp import FastMCP # Use FastMCP
from typing import Dict, Any

# Configure base logger (handlers will be added by server.py)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Create the MCP server instance using FastMCP
mcp = FastMCP("PAELLADOC")

# --- Register Tools/Prompts --- #

# Import plugins dynamically to register tools/prompts
try:
    # Import from the new adapters location
    import paelladoc.adapters.plugins
    logger.info("Successfully loaded plugins from paelladoc.adapters.plugins")
except ImportError as e:
    # Log as warning, server might still be usable with base tools
    logger.warning(f"Could not import plugins from paelladoc.adapters.plugins: {e}")
except Exception as e:
    # Log as error for unexpected issues during import
    logger.error(f"An unexpected error occurred during plugin import: {e}", exc_info=True)

@mcp.tool() # Use decorator again
def ping(random_string: str = "") -> Dict[str, Any]:
    """
    Basic health check; returns pong.
    
    Args:
        random_string (str, optional): Dummy parameter for no-parameter tools

    Returns:
        Dict[str, Any]: Response with status and message
    """
    logger.debug(f"Ping tool called with parameter: {random_string}")
    return {
        "status": "ok", 
        "message": "pong"
    }

# Tools will be registered here by plugins

# Note: No `if __name__ == "__main__":` block here.
# This file is intended to be imported by the entry point (server.py).

================
File: paelladoc/domain/models/project.py
================
from enum import Enum
from typing import List, Dict, Optional, Set
from pydantic import BaseModel, Field
import datetime
from pathlib import Path

class DocumentStatus(str, Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"

class Bucket(str, Enum):
    """MECE taxonomy buckets for categorizing artifacts"""
    
    # Initiate categories
    INITIATE_CORE_SETUP = "Initiate::CoreSetup"
    INITIATE_INITIAL_PRODUCT_DOCS = "Initiate::InitialProductDocs"
    
    # Elaborate categories
    ELABORATE_DISCOVERY_AND_RESEARCH = "Elaborate::DiscoveryAndResearch"
    ELABORATE_IDEATION_AND_DESIGN = "Elaborate::IdeationAndDesign"
    ELABORATE_SPECIFICATION_AND_PLANNING = "Elaborate::SpecificationAndPlanning"
    ELABORATE_CORE_AND_SUPPORT = "Elaborate::CoreAndSupport"
    
    # Govern categories
    GOVERN_CORE_SYSTEM = "Govern::CoreSystem"
    GOVERN_STANDARDS_METHODOLOGIES = "Govern::StandardsMethodologies"
    GOVERN_VERIFICATION_VALIDATION = "Govern::VerificationValidation"
    GOVERN_MEMORY_TEMPLATES = "Govern::MemoryTemplates"
    GOVERN_TOOLING_SCRIPTS = "Govern::ToolingScripts"
    
    # Generate categories
    GENERATE_CORE_FUNCTIONALITY = "Generate::CoreFunctionality"
    GENERATE_SUPPORTING_ELEMENTS = "Generate::SupportingElements"
    
    # Maintain categories
    MAINTAIN_CORE_FUNCTIONALITY = "Maintain::CoreFunctionality"
    MAINTAIN_SUPPORTING_ELEMENTS = "Maintain::SupportingElements"
    
    # Deploy categories
    DEPLOY_PIPELINES_AND_AUTOMATION = "Deploy::PipelinesAndAutomation"
    DEPLOY_INFRASTRUCTURE_AND_CONFIG = "Deploy::InfrastructureAndConfig"
    DEPLOY_GUIDES_AND_CHECKLISTS = "Deploy::GuidesAndChecklists"
    DEPLOY_SECURITY = "Deploy::Security"
    
    # Operate categories
    OPERATE_RUNBOOKS_AND_SOPS = "Operate::RunbooksAndSOPs"
    OPERATE_MONITORING_AND_ALERTING = "Operate::MonitoringAndAlerting"
    OPERATE_MAINTENANCE = "Operate::Maintenance"
    
    # Iterate categories
    ITERATE_LEARNING_AND_ANALYSIS = "Iterate::LearningAndAnalysis"
    ITERATE_PLANNING_AND_RETROSPECTION = "Iterate::PlanningAndRetrospection"
    
    # Special bucket for artifacts not matching any pattern
    UNKNOWN = "Unknown"
    
    @classmethod
    def get_phase_buckets(cls, phase: str) -> Set['Bucket']:
        """Get all buckets belonging to a specific phase"""
        return {bucket for bucket in cls if bucket.value.startswith(f"{phase}::")}

class ProjectDocument(BaseModel):
    name: str # e.g., "README.md", "CONTRIBUTING.md"
    template_origin: Optional[str] = None # Path or identifier of the template used
    status: DocumentStatus = DocumentStatus.PENDING

class ArtifactMeta(BaseModel):
    """Metadata for an artifact categorized according to the MECE taxonomy"""
    name: str
    bucket: Bucket
    path: Path  # Relative path from project root
    created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
    updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
    status: DocumentStatus = DocumentStatus.PENDING
    
    def update_timestamp(self):
        self.updated_at = datetime.datetime.now()
    
    def update_status(self, status: DocumentStatus):
        self.status = status
        self.update_timestamp()

class ProjectMetadata(BaseModel):
    name: str = Field(..., description="Unique name of the project")
    language: Optional[str] = None
    purpose: Optional[str] = None
    target_audience: Optional[str] = None
    objectives: Optional[List[str]] = None
    # Add other relevant metadata fields as needed

class ProjectMemory(BaseModel):
    metadata: ProjectMetadata
    documents: Dict[str, ProjectDocument] = {} # Dict key is document name/path
    # New taxonomy-based structure
    taxonomy_version: str = "0.5"
    artifacts: Dict[Bucket, List[ArtifactMeta]] = Field(default_factory=lambda: {bucket: [] for bucket in Bucket})
    # Consider adding: achievements, issues, decisions later?
    created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
    last_updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)

    def update_timestamp(self):
        self.last_updated_at = datetime.datetime.now()

    def get_document(self, name: str) -> Optional[ProjectDocument]:
        return self.documents.get(name)

    def update_document_status(self, name: str, status: DocumentStatus):
        doc = self.get_document(name)
        if doc:
            doc.status = status
            self.update_timestamp()
        else:
            # TODO: Decide error handling (log or raise?)
            # For now, just pass
            # Consider logging: logger.warning(f"Attempted to update status for non-existent document: {name}")
            pass 

    def add_document(self, doc: ProjectDocument):
         if doc.name not in self.documents:
             self.documents[doc.name] = doc
             self.update_timestamp()
         else:
             # TODO: Decide error handling (log or raise?)
             # For now, just pass
             # Consider logging: logger.warning(f"Attempted to add duplicate document: {doc.name}")
             pass
             
    # New methods for artifact management
    def get_artifact(self, bucket: Bucket, name: str) -> Optional[ArtifactMeta]:
        """Get an artifact by bucket and name"""
        for artifact in self.artifacts.get(bucket, []):
            if artifact.name == name:
                return artifact
        return None
    
    def get_artifact_by_path(self, path: Path) -> Optional[ArtifactMeta]:
        """Get an artifact by path, searching across all buckets"""
        path_str = str(path)
        for bucket_artifacts in self.artifacts.values():
            for artifact in bucket_artifacts:
                if str(artifact.path) == path_str:
                    return artifact
        return None
    
    def add_artifact(self, artifact: ArtifactMeta) -> bool:
        """Add an artifact to the appropriate bucket. Returns True if added, False if duplicate."""
        bucket = artifact.bucket
        if bucket not in self.artifacts:
            self.artifacts[bucket] = []
            
        # Check if artifact with same path already exists in any bucket
        existing = self.get_artifact_by_path(artifact.path)
        if existing:
            # TODO: Decide error handling (log or raise?)
            # For now, just return False
            return False
            
        self.artifacts[bucket].append(artifact)
        self.update_timestamp()
        return True
    
    def update_artifact_status(self, bucket: Bucket, name: str, status: DocumentStatus) -> bool:
        """Update an artifact's status. Returns True if updated, False if not found."""
        artifact = self.get_artifact(bucket, name)
        if artifact:
            artifact.update_status(status)
            self.update_timestamp()
            return True
        return False
    
    def get_bucket_completion(self, bucket: Bucket) -> dict:
        """Get completion stats for a bucket"""
        artifacts = self.artifacts.get(bucket, [])
        total = len(artifacts)
        completed = sum(1 for a in artifacts if a.status == DocumentStatus.COMPLETED)
        in_progress = sum(1 for a in artifacts if a.status == DocumentStatus.IN_PROGRESS)
        pending = total - completed - in_progress
        
        return {
            "total": total,
            "completed": completed,
            "in_progress": in_progress,
            "pending": pending,
            "completion_percentage": (completed / total * 100) if total > 0 else 0
        }
    
    def get_phase_completion(self, phase: str) -> dict:
        """Get completion stats for an entire phase"""
        phase_buckets = Bucket.get_phase_buckets(phase)
        
        total = 0
        completed = 0
        in_progress = 0
        
        for bucket in phase_buckets:
            stats = self.get_bucket_completion(bucket)
            total += stats["total"]
            completed += stats["completed"]
            in_progress += stats["in_progress"]
        
        pending = total - completed - in_progress
        
        return {
            "phase": phase,
            "buckets": len(phase_buckets),
            "total": total,
            "completed": completed,
            "in_progress": in_progress,
            "pending": pending,
            "completion_percentage": (completed / total * 100) if total > 0 else 0
        }

================
File: tests/README.md
================
# MCP Server Tests

This directory contains tests for the Paelladoc MCP server following hexagonal architecture principles. Tests are organized into three main categories:

## Test Structure

```
tests/
├── unit/            # Unit tests for individual components
│   └── test_tools.py  # Tests for MCP tools in isolation
├── integration/     # Integration tests for component interactions
│   └── test_server.py # Tests for server STDIO communication
└── e2e/             # End-to-end tests simulating real-world usage
    └── test_cursor_simulation.py # Simulates Cursor interaction
```

## Test Categories

1. **Unit Tests** (`unit/`)
   - Test individual functions/components in isolation
   - Don't require a running server
   - Fast to execute
   - Example: Testing the `ping()` function directly

2. **Integration Tests** (`integration/`)
   - Test interactions between components
   - Verify STDIO communication with the server
   - Example: Starting the server and sending/receiving messages

3. **End-to-End Tests** (`e2e/`)
   - Simulate real-world usage scenarios
   - Test the system as a whole
   - Example: Simulating how Cursor would interact with the server

## Running Tests

### Run All Tests

```bash
python -m unittest discover mcp_server/tests
```

### Run Tests by Category

```bash
# Unit tests only
python -m unittest discover mcp_server/tests/unit

# Integration tests only
python -m unittest discover mcp_server/tests/integration

# End-to-end tests only
python -m unittest discover mcp_server/tests/e2e
```

### Run a Specific Test File

```bash
python -m unittest mcp_server/tests/unit/test_tools.py
```

### Run a Specific Test Case

```bash
python -m unittest mcp_server.tests.unit.test_tools.TestToolsPing
```

### Run a Specific Test Method

```bash
python -m unittest mcp_server.tests.unit.test_tools.TestToolsPing.test_ping_returns_dict
```

## TDD Process

These tests follow the Test-Driven Development (TDD) approach:

1. **RED**: Write failing tests first
2. **GREEN**: Implement the minimal code to make tests pass
3. **REFACTOR**: Improve the code while keeping tests passing

## Adding New Tests

When adding new MCP tools:

1. Create unit tests for the tool's functionality
2. Add integration tests for the tool's STDIO communication
3. Update E2E tests to verify Cursor interaction with the tool

================
File: tests/unit/test_ping_tool.py
================
"""
Unit tests for Paelladoc MCP tools.

Following TDD approach - tests are written before implementation.
"""

import unittest
import sys
import os
from pathlib import Path

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.absolute()
if str(project_root) not in sys.path:
    sys.path.insert(0, str(project_root))

# Import directly from the domain layer
from paelladoc.domain import core_logic

class TestPingTool(unittest.TestCase):
    """Unit tests for the ping tool following TDD methodology."""
    
    def test_ping_exists(self):
        """Test that the ping function exists."""
        self.assertTrue(hasattr(core_logic, "ping"), 
                       "The ping function does not exist in core_logic")
    
    def test_ping_returns_dict(self):
        """Test that ping returns a dictionary."""
        result = core_logic.ping()
        self.assertIsInstance(result, dict, 
                             "ping should return a dictionary")
    
    def test_ping_has_required_fields(self):
        """Test that ping response has the required fields."""
        result = core_logic.ping()
        self.assertIn("status", result, 
                     "ping response should contain a 'status' field")
        self.assertIn("message", result, 
                     "ping response should contain a 'message' field")
    
    def test_ping_returns_expected_values(self):
        """Test that ping returns the expected values."""
        result = core_logic.ping()
        self.assertEqual(result["status"], "ok", 
                        f"ping status should be 'ok', got '{result['status']}'")
        self.assertEqual(result["message"], "pong", 
                        f"ping message should be 'pong', got '{result['message']}'")

if __name__ == "__main__":
    unittest.main()

================
File: tests/unit/application/utils/test_behavior_enforcer.py
================
"""
Unit tests for the BehaviorEnforcer utility.
"""

import unittest
from unittest.mock import MagicMock
import sys
from pathlib import Path
from typing import Dict, Any, Set, Optional, List

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.parent.parent.absolute()
sys.path.insert(0, str(project_root))

# Module to test
from paelladoc.application.utils.behavior_enforcer import BehaviorEnforcer, BehaviorViolationError

# Mock context object for tests
class MockContext:
    def __init__(self, collected_params: Optional[Set[str]] = None):
        self.progress = {"collected_params": collected_params if collected_params is not None else set()}

class TestBehaviorEnforcer(unittest.TestCase):
    """Unit tests for the BehaviorEnforcer."""

    def setUp(self):
        self.tool_name = "test.tool"
        self.sequence = ["param1", "param2", "param3"]
        self.behavior_config = {"fixed_question_order": self.sequence}

    def test_enforce_no_config(self):
        """Test that enforcement passes if no behavior_config is provided."""
        try:
            BehaviorEnforcer.enforce(self.tool_name, None, MockContext(), {"arg": 1})
        except BehaviorViolationError:
            self.fail("Enforcement should pass when no config is given.")

    def test_enforce_no_fixed_order(self):
        """Test enforcement passes if 'fixed_question_order' is not in config."""
        config = {"other_rule": True}
        try:
            BehaviorEnforcer.enforce(self.tool_name, config, MockContext(), {"param1": "value"})
        except BehaviorViolationError:
            self.fail("Enforcement should pass when fixed_question_order is not defined.")
            
    def test_enforce_no_context_or_args(self):
        """Test enforcement passes (logs warning) if context or args are missing."""
        # Note: Current implementation returns None (passes), might change behavior later.
        try:
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, None, None)
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, MockContext(), None)
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, None, {"param1": "a"})
        except BehaviorViolationError:
            self.fail("Enforcement should pass when context or args are missing.")
            
    def test_enforce_no_new_params_provided(self):
        """Test enforcement passes if no *new* parameters are provided."""
        ctx = MockContext(collected_params={"param1"})
        # Providing only already collected param
        provided_args = {"param1": "new_value", "param2": None} 
        try:
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)
        except BehaviorViolationError as e:
            self.fail(f"Enforcement should pass when only old params are provided. Raised: {e}")

    def test_enforce_correct_first_param(self):
        """Test enforcement passes when the correct first parameter is provided."""
        ctx = MockContext()
        provided_args = {"param1": "value1"}
        try:
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)
        except BehaviorViolationError as e:
            self.fail(f"Enforcement failed for correct first param. Raised: {e}")

    def test_enforce_correct_second_param(self):
        """Test enforcement passes when the correct second parameter is provided."""
        ctx = MockContext(collected_params={"param1"})
        provided_args = {"param1": "value1", "param2": "value2"} # param1 is old, param2 is new
        try:
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)
        except BehaviorViolationError as e:
            self.fail(f"Enforcement failed for correct second param. Raised: {e}")

    def test_enforce_incorrect_first_param(self):
        """Test enforcement fails when the wrong first parameter is provided."""
        ctx = MockContext()
        provided_args = {"param2": "value2"} # Should be param1
        with self.assertRaisesRegex(BehaviorViolationError, "Expected next: 'param1'. Got unexpected new parameter: 'param2'"):
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)

    def test_enforce_incorrect_second_param(self):
        """Test enforcement fails when the wrong second parameter is provided."""
        ctx = MockContext(collected_params={"param1"})
        provided_args = {"param1": "val1", "param3": "value3"} # Should be param2
        with self.assertRaisesRegex(BehaviorViolationError, "Expected next: 'param2'. Got unexpected new parameter: 'param3'"):
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)
            
    def test_enforce_multiple_new_params_fails(self):
        """Test enforcement fails when multiple new parameters are provided at once."""
        ctx = MockContext()
        provided_args = {"param1": "value1", "param2": "value2"} # Both are new
        # Adjust regex to match the more detailed error message
        expected_regex = (
            r"Tool 'test.tool' expects parameters sequentially. "
            r"Expected next: 'param1'. "
            # Use regex to handle potential set order variations {'param1', 'param2'} or {'param2', 'param1'}
            r"Provided multiple new parameters: {('param1', 'param2'|'param2', 'param1')}. "
            r"Collected so far: set\(\)."
        )
        with self.assertRaisesRegex(BehaviorViolationError, expected_regex):
             BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)
             
    def test_enforce_multiple_new_params_later_fails(self):
        """Test enforcement fails when multiple new params are provided later in sequence."""
        ctx = MockContext(collected_params={"param1"})
        provided_args = {"param1":"v1", "param2": "value2", "param3": "value3"} # param2 and param3 are new
        # Adjust regex to match the more detailed error message
        expected_regex = (
            r"Tool 'test.tool' expects parameters sequentially. "
            r"Expected next: 'param2'. "
            # Use regex to handle potential set order variations
            r"Provided multiple new parameters: {('param2', 'param3'|'param3', 'param2')}. " 
            r"Collected so far: {'param1'}."
        )
        with self.assertRaisesRegex(BehaviorViolationError, expected_regex):
             BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)

    def test_enforce_params_after_sequence_complete_passes(self):
        """Test enforcement passes when providing args after the sequence is complete."""
        ctx = MockContext(collected_params={"param1", "param2", "param3"})
        provided_args = {"param1":"v1", "param2":"v2", "param3":"v3", "optional_param": "opt"}
        try:
            BehaviorEnforcer.enforce(self.tool_name, self.behavior_config, ctx, provided_args)
        except BehaviorViolationError as e:
             self.fail(f"Enforcement should pass for args after sequence complete. Raised: {e}")

# if __name__ == "__main__":
#     unittest.main()

================
File: tests/unit/application/services/test_memory_service.py
================
"""
Unit tests for the MemoryService.
"""

import unittest
from unittest.mock import MagicMock, AsyncMock # Use AsyncMock for async methods
import sys
from pathlib import Path
import datetime

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.parent.parent.absolute()
sys.path.insert(0, str(project_root))

# Modules to test
from paelladoc.application.services.memory_service import MemoryService
from paelladoc.domain.models.project import ProjectMemory, ProjectMetadata, ProjectDocument, DocumentStatus
from paelladoc.ports.output.memory_port import MemoryPort

class TestMemoryService(unittest.IsolatedAsyncioTestCase):
    """Unit tests for the MemoryService using a mocked MemoryPort."""

    def setUp(self):
        """Set up a mocked MemoryPort before each test."""
        # Create a mock object that adheres to the MemoryPort interface
        self.mock_memory_port = AsyncMock(spec=MemoryPort)
        # Instantiate the service with the mock
        self.memory_service = MemoryService(memory_port=self.mock_memory_port)
        
    def _create_sample_memory(self, name: str) -> ProjectMemory:
        """Helper to create a sample ProjectMemory object for testing."""
        metadata = ProjectMetadata(name=name, language="test-lang")
        doc1 = ProjectDocument(name="doc1.md", status=DocumentStatus.PENDING)
        documents = {doc1.name: doc1}
        return ProjectMemory(metadata=metadata, documents=documents)

    # --- Test Cases --- #

    async def test_get_project_memory_calls_port(self):
        """Verify get_project_memory calls load_memory on the port."""
        project_name = "test-get"
        expected_memory = self._create_sample_memory(project_name)
        self.mock_memory_port.load_memory.return_value = expected_memory
        
        actual_memory = await self.memory_service.get_project_memory(project_name)
        
        self.mock_memory_port.load_memory.assert_awaited_once_with(project_name)
        self.assertEqual(actual_memory, expected_memory)

    async def test_get_project_memory_not_found(self):
        """Verify get_project_memory returns None if port returns None."""
        project_name = "test-get-none"
        self.mock_memory_port.load_memory.return_value = None
        
        actual_memory = await self.memory_service.get_project_memory(project_name)
        
        self.mock_memory_port.load_memory.assert_awaited_once_with(project_name)
        self.assertIsNone(actual_memory)

    async def test_check_project_exists_calls_port(self):
        """Verify check_project_exists calls project_exists on the port."""
        project_name = "test-exists"
        self.mock_memory_port.project_exists.return_value = True
        
        exists = await self.memory_service.check_project_exists(project_name)
        
        self.mock_memory_port.project_exists.assert_awaited_once_with(project_name)
        self.assertTrue(exists)
        
    async def test_check_project_not_exists_calls_port(self):
        """Verify check_project_exists calls project_exists on the port (False case)."""
        project_name = "test-not-exists"
        self.mock_memory_port.project_exists.return_value = False
        
        exists = await self.memory_service.check_project_exists(project_name)
        
        self.mock_memory_port.project_exists.assert_awaited_once_with(project_name)
        self.assertFalse(exists)
        
    async def test_create_project_memory_success(self):
        """Verify create_project_memory calls exists and save on the port when project doesn't exist."""
        project_name = "test-create"
        memory_to_create = self._create_sample_memory(project_name)
        
        # Mock project_exists to return False (project doesn't exist initially)
        self.mock_memory_port.project_exists.return_value = False
        # Mock save_memory to do nothing (or return None, as it's typed)
        self.mock_memory_port.save_memory.return_value = None 
        
        created_memory = await self.memory_service.create_project_memory(memory_to_create)
        
        # Assertions
        self.mock_memory_port.project_exists.assert_awaited_once_with(project_name)
        self.mock_memory_port.save_memory.assert_awaited_once_with(memory_to_create)
        self.assertEqual(created_memory, memory_to_create)

    async def test_create_project_memory_already_exists_raises_error(self):
        """Verify create_project_memory raises ValueError if project exists."""
        project_name = "test-create-exists"
        memory_to_create = self._create_sample_memory(project_name)
        
        # Mock project_exists to return True
        self.mock_memory_port.project_exists.return_value = True
        
        with self.assertRaisesRegex(ValueError, f"Project memory for '{project_name}' already exists."):
            await self.memory_service.create_project_memory(memory_to_create)
            
        # Assertions
        self.mock_memory_port.project_exists.assert_awaited_once_with(project_name)
        self.mock_memory_port.save_memory.assert_not_awaited() # Save should not be called

    async def test_update_project_memory_success(self):
        """Verify update_project_memory calls exists and save when project exists."""
        project_name = "test-update"
        memory_to_update = self._create_sample_memory(project_name)
        
        # Mock project_exists to return True
        self.mock_memory_port.project_exists.return_value = True
        self.mock_memory_port.save_memory.return_value = None
        
        updated_memory = await self.memory_service.update_project_memory(memory_to_update)
        
        # Assertions
        self.mock_memory_port.project_exists.assert_awaited_once_with(project_name)
        self.mock_memory_port.save_memory.assert_awaited_once_with(memory_to_update)
        self.assertEqual(updated_memory, memory_to_update)

    async def test_update_project_memory_does_not_exist_raises_error(self):
        """Verify update_project_memory raises ValueError if project does not exist."""
        project_name = "test-update-not-exists"
        memory_to_update = self._create_sample_memory(project_name)
        
        # Mock project_exists to return False
        self.mock_memory_port.project_exists.return_value = False
        
        with self.assertRaisesRegex(ValueError, f"Project memory for '{project_name}' does not exist."):
            await self.memory_service.update_project_memory(memory_to_update)
            
        # Assertions
        self.mock_memory_port.project_exists.assert_awaited_once_with(project_name)
        self.mock_memory_port.save_memory.assert_not_awaited()

    async def test_update_document_status_success(self):
        """Verify update_document_status loads, updates domain model, and saves."""
        project_name = "test-update-doc"
        doc_name = "doc1.md"
        new_status = DocumentStatus.COMPLETED
        original_memory = self._create_sample_memory(project_name)
        
        # Mock load_memory to return the original memory
        self.mock_memory_port.load_memory.return_value = original_memory
        self.mock_memory_port.save_memory.return_value = None
        
        updated_memory = await self.memory_service.update_document_status_in_memory(
            project_name, doc_name, new_status
        )
        
        # Assertions
        self.mock_memory_port.load_memory.assert_awaited_once_with(project_name)
        # Check that save was called with the *modified* memory object
        self.mock_memory_port.save_memory.assert_awaited_once() 
        saved_arg = self.mock_memory_port.save_memory.call_args[0][0]
        self.assertEqual(saved_arg.documents[doc_name].status, new_status)
        self.assertIsNotNone(updated_memory)
        self.assertEqual(updated_memory.documents[doc_name].status, new_status)

    async def test_update_document_status_project_not_found(self):
        """Verify update_document_status returns None if project not found."""
        project_name = "test-update-doc-no-proj"
        doc_name = "doc1.md"
        new_status = DocumentStatus.COMPLETED
        
        self.mock_memory_port.load_memory.return_value = None # Project not found
        
        result = await self.memory_service.update_document_status_in_memory(
            project_name, doc_name, new_status
        )
        
        self.mock_memory_port.load_memory.assert_awaited_once_with(project_name)
        self.mock_memory_port.save_memory.assert_not_awaited()
        self.assertIsNone(result)
        
    async def test_update_document_status_doc_not_found(self):
        """Verify update_document_status returns original memory if doc not found."""
        project_name = "test-update-doc-no-doc"
        doc_name = "nonexistent.md"
        new_status = DocumentStatus.COMPLETED
        original_memory = self._create_sample_memory(project_name)
        
        self.mock_memory_port.load_memory.return_value = original_memory
        
        result = await self.memory_service.update_document_status_in_memory(
            project_name, doc_name, new_status
        )
        
        self.mock_memory_port.load_memory.assert_awaited_once_with(project_name)
        # Save should NOT be called if the doc wasn't found to update
        self.mock_memory_port.save_memory.assert_not_awaited()
        self.assertEqual(result, original_memory) # Should return unchanged memory

# if __name__ == "__main__":
#     unittest.main()

================
File: tests/unit/application/services/test_vector_store_service.py
================
"""
Unit tests for the VectorStoreService.
"""

import unittest
from unittest.mock import AsyncMock, MagicMock # Added MagicMock for SearchResult
import sys
from pathlib import Path
from typing import List, Dict, Any, Optional

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.parent.parent.absolute()
sys.path.insert(0, str(project_root))

# Modules to test
from paelladoc.application.services.vector_store_service import VectorStoreService
from paelladoc.ports.output.vector_store_port import VectorStorePort, SearchResult

# Dummy SearchResult implementation for tests
class MockSearchResult(SearchResult):
    def __init__(self, id: str, distance: float, metadata: Dict[str, Any], document: str):
        self.id = id
        self.distance = distance
        self.metadata = metadata
        self.document = document

class TestVectorStoreService(unittest.IsolatedAsyncioTestCase):
    """Unit tests for the VectorStoreService using a mocked VectorStorePort."""

    def setUp(self):
        """Set up a mocked VectorStorePort before each test."""
        self.mock_vector_store_port = AsyncMock(spec=VectorStorePort)
        self.vector_store_service = VectorStoreService(vector_store_port=self.mock_vector_store_port)

    # --- Test Cases --- #

    async def test_add_texts_to_collection_calls_port(self):
        """Verify add_texts_to_collection calls add_documents on the port."""
        collection_name = "test_coll"
        documents = ["doc1", "doc2"]
        metadatas = [{"s": 1}, {"s": 2}]
        ids = ["id1", "id2"]
        expected_ids = ids

        self.mock_vector_store_port.add_documents.return_value = expected_ids

        actual_ids = await self.vector_store_service.add_texts_to_collection(
            collection_name, documents, metadatas, ids
        )

        self.mock_vector_store_port.add_documents.assert_awaited_once_with(
            collection_name=collection_name,
            documents=documents,
            metadatas=metadatas,
            ids=ids
        )
        self.assertEqual(actual_ids, expected_ids)
        
    async def test_add_texts_to_collection_reraises_exception(self):
        """Verify add_texts_to_collection re-raises port exceptions."""
        collection_name = "test_coll_fail"
        documents = ["doc1"]
        test_exception = ValueError("Port error")
        self.mock_vector_store_port.add_documents.side_effect = test_exception

        with self.assertRaises(ValueError) as cm:
            await self.vector_store_service.add_texts_to_collection(collection_name, documents)
        
        self.assertEqual(cm.exception, test_exception)
        self.mock_vector_store_port.add_documents.assert_awaited_once()

    async def test_find_similar_texts_calls_port(self):
        """Verify find_similar_texts calls search_similar on the port."""
        collection_name = "test_search_coll"
        query_texts = ["query1"]
        n_results = 3
        filter_metadata = {"year": 2024}
        filter_document = None # Example
        expected_results: List[List[SearchResult]] = [
            [MockSearchResult("res1", 0.5, {"year": 2024}, "doc text")]
        ]

        self.mock_vector_store_port.search_similar.return_value = expected_results

        actual_results = await self.vector_store_service.find_similar_texts(
            collection_name, query_texts, n_results, filter_metadata, filter_document
        )

        self.mock_vector_store_port.search_similar.assert_awaited_once_with(
            collection_name=collection_name,
            query_texts=query_texts,
            n_results=n_results,
            where=filter_metadata,
            where_document=filter_document,
            include=["metadatas", "documents", "distances", "ids"] # Check default include
        )
        self.assertEqual(actual_results, expected_results)
        
    async def test_find_similar_texts_reraises_exception(self):
        """Verify find_similar_texts re-raises port exceptions."""
        collection_name = "test_search_fail"
        query_texts = ["query1"]
        test_exception = RuntimeError("Search failed")
        self.mock_vector_store_port.search_similar.side_effect = test_exception
        
        with self.assertRaises(RuntimeError) as cm:
            await self.vector_store_service.find_similar_texts(collection_name, query_texts)
        
        self.assertEqual(cm.exception, test_exception)
        self.mock_vector_store_port.search_similar.assert_awaited_once()

    async def test_ensure_collection_exists_calls_port(self):
        """Verify ensure_collection_exists calls get_or_create_collection on the port."""
        collection_name = "ensure_coll"
        # Mock the port method to return a dummy collection object (can be anything)
        self.mock_vector_store_port.get_or_create_collection.return_value = MagicMock()

        await self.vector_store_service.ensure_collection_exists(collection_name)

        self.mock_vector_store_port.get_or_create_collection.assert_awaited_once_with(collection_name)
        
    async def test_ensure_collection_exists_reraises_exception(self):
        """Verify ensure_collection_exists re-raises port exceptions."""
        collection_name = "ensure_coll_fail"
        test_exception = ConnectionError("DB down")
        self.mock_vector_store_port.get_or_create_collection.side_effect = test_exception

        with self.assertRaises(ConnectionError) as cm:
            await self.vector_store_service.ensure_collection_exists(collection_name)
            
        self.assertEqual(cm.exception, test_exception)
        self.mock_vector_store_port.get_or_create_collection.assert_awaited_once_with(collection_name)

    async def test_remove_collection_calls_port(self):
        """Verify remove_collection calls delete_collection on the port."""
        collection_name = "remove_coll"
        self.mock_vector_store_port.delete_collection.return_value = None # Method returns None

        await self.vector_store_service.remove_collection(collection_name)

        self.mock_vector_store_port.delete_collection.assert_awaited_once_with(collection_name)

    async def test_remove_collection_reraises_exception(self):
        """Verify remove_collection re-raises port exceptions."""
        collection_name = "remove_coll_fail"
        test_exception = TimeoutError("Delete timed out")
        self.mock_vector_store_port.delete_collection.side_effect = test_exception

        with self.assertRaises(TimeoutError) as cm:
             await self.vector_store_service.remove_collection(collection_name)
        
        self.assertEqual(cm.exception, test_exception)
        self.mock_vector_store_port.delete_collection.assert_awaited_once_with(collection_name)

# if __name__ == "__main__":
#     unittest.main()

================
File: tests/unit/domain/models/test_project.py
================
import json
import pytest
from datetime import datetime
from pathlib import Path

from paelladoc.domain.models.project import (
    DocumentStatus,
    Bucket,
    ArtifactMeta,
    ProjectMetadata,
    ProjectMemory
)

class TestBucket:
    """Tests for the Bucket enum"""

    def test_bucket_values(self):
        """Test that all buckets have the correct string format"""
        for bucket in Bucket:
            if bucket is not Bucket.UNKNOWN:
                # Format should be "Phase::Subcategory"
                assert "::" in bucket.value
                phase, subcategory = bucket.value.split("::")
                assert phase in ["Initiate", "Elaborate", "Govern", "Generate", "Maintain", "Deploy", "Operate", "Iterate"]
                assert len(subcategory) > 0
            else:
                assert bucket.value == "Unknown"

    def test_get_phase_buckets(self):
        """Test the get_phase_buckets class method"""
        initiate_buckets = Bucket.get_phase_buckets("Initiate")
        assert len(initiate_buckets) == 2
        assert Bucket.INITIATE_CORE_SETUP in initiate_buckets
        assert Bucket.INITIATE_INITIAL_PRODUCT_DOCS in initiate_buckets
        
        elaborate_buckets = Bucket.get_phase_buckets("Elaborate")
        assert len(elaborate_buckets) == 4
        
        # Should return empty set for non-existent phase
        nonexistent_buckets = Bucket.get_phase_buckets("NonExistent")
        assert len(nonexistent_buckets) == 0

class TestArtifactMeta:
    """Tests for the ArtifactMeta model"""
    
    def test_create_artifact_meta(self):
        """Test creating an ArtifactMeta instance"""
        artifact = ArtifactMeta(
            name="test_artifact",
            bucket=Bucket.INITIATE_CORE_SETUP,
            path=Path("docs/test_artifact.md"),
            status=DocumentStatus.IN_PROGRESS
        )
        
        assert artifact.name == "test_artifact"
        assert artifact.bucket == Bucket.INITIATE_CORE_SETUP
        assert artifact.path == Path("docs/test_artifact.md")
        assert artifact.status == DocumentStatus.IN_PROGRESS
        assert isinstance(artifact.created_at, datetime)
        assert isinstance(artifact.updated_at, datetime)
    
    def test_update_status(self):
        """Test updating an artifact's status"""
        artifact = ArtifactMeta(
            name="test_artifact",
            bucket=Bucket.INITIATE_CORE_SETUP,
            path=Path("docs/test_artifact.md")
        )
        
        # Default status should be PENDING
        assert artifact.status == DocumentStatus.PENDING
        
        # Store the original timestamp
        original_updated_at = artifact.updated_at
        
        # Update the status
        artifact.update_status(DocumentStatus.COMPLETED)
        
        # Check that status was updated
        assert artifact.status == DocumentStatus.COMPLETED
        
        # Check that timestamp was updated
        assert artifact.updated_at > original_updated_at
    
    def test_serialization_deserialization(self):
        """Test that ArtifactMeta can be serialized and deserialized"""
        artifact = ArtifactMeta(
            name="test_artifact",
            bucket=Bucket.ELABORATE_DISCOVERY_AND_RESEARCH,
            path=Path("docs/research/test_artifact.md"),
            status=DocumentStatus.COMPLETED
        )
        
        # Serialize to JSON
        artifact_json = artifact.model_dump_json()
        
        # Deserialize from JSON
        loaded_artifact = ArtifactMeta.model_validate_json(artifact_json)
        
        # Check that all fields were preserved
        assert loaded_artifact.name == artifact.name
        assert loaded_artifact.bucket == artifact.bucket
        assert loaded_artifact.path == artifact.path
        assert loaded_artifact.status == artifact.status
        assert loaded_artifact.created_at == artifact.created_at
        assert loaded_artifact.updated_at == artifact.updated_at

class TestProjectMemory:
    """Tests for the ProjectMemory model with taxonomy support"""
    
    @pytest.fixture
    def sample_project_memory(self):
        """Create a sample ProjectMemory with artifacts in multiple buckets"""
        project = ProjectMemory(
            metadata=ProjectMetadata(name="test_project"),
            taxonomy_version="0.5"
        )
        
        # Add artifacts to multiple buckets
        artifacts = [
            ArtifactMeta(
                name="vision_doc",
                bucket=Bucket.INITIATE_INITIAL_PRODUCT_DOCS,
                path=Path("docs/initiation/product_vision.md")
            ),
            ArtifactMeta(
                name="user_research",
                bucket=Bucket.ELABORATE_DISCOVERY_AND_RESEARCH,
                path=Path("docs/research/user_research.md"),
                status=DocumentStatus.IN_PROGRESS
            ),
            ArtifactMeta(
                name="api_spec",
                bucket=Bucket.ELABORATE_SPECIFICATION_AND_PLANNING,
                path=Path("docs/specs/api_specification.md"),
                status=DocumentStatus.COMPLETED
            )
        ]
        
        for artifact in artifacts:
            project.add_artifact(artifact)
            
        return project
    
    def test_project_memory_initialization(self):
        """Test initializing ProjectMemory with taxonomy support"""
        project = ProjectMemory(
            metadata=ProjectMetadata(name="test_project"),
            taxonomy_version="0.5"
        )
        
        # Check that all buckets are initialized
        for bucket in Bucket:
            assert bucket in project.artifacts
            assert isinstance(project.artifacts[bucket], list)
            assert len(project.artifacts[bucket]) == 0
    
    def test_add_artifact(self, sample_project_memory):
        """Test adding artifacts to ProjectMemory"""
        project = sample_project_memory
        
        # Check that artifacts were added to the correct buckets
        assert len(project.artifacts[Bucket.INITIATE_INITIAL_PRODUCT_DOCS]) == 1
        assert len(project.artifacts[Bucket.ELABORATE_DISCOVERY_AND_RESEARCH]) == 1
        assert len(project.artifacts[Bucket.ELABORATE_SPECIFICATION_AND_PLANNING]) == 1
        
        # Check that artifact was added with correct fields
        initiate_artifact = project.artifacts[Bucket.INITIATE_INITIAL_PRODUCT_DOCS][0]
        assert initiate_artifact.name == "vision_doc"
        assert initiate_artifact.path == Path("docs/initiation/product_vision.md")
        assert initiate_artifact.status == DocumentStatus.PENDING
        
        # Test adding a duplicate (should return False)
        duplicate = ArtifactMeta(
            name="dup_vision",
            bucket=Bucket.INITIATE_CORE_SETUP,
            path=Path("docs/initiation/product_vision.md")  # Same path as existing artifact
        )
        assert project.add_artifact(duplicate) == False
        
        # Check that original buckets still have the same count
        assert len(project.artifacts[Bucket.INITIATE_INITIAL_PRODUCT_DOCS]) == 1
        assert len(project.artifacts[Bucket.INITIATE_CORE_SETUP]) == 0  # Duplicate wasn't added
    
    def test_get_artifact(self, sample_project_memory):
        """Test retrieving artifacts by bucket and name"""
        project = sample_project_memory
        
        # Get existing artifact
        artifact = project.get_artifact(Bucket.ELABORATE_DISCOVERY_AND_RESEARCH, "user_research")
        assert artifact is not None
        assert artifact.name == "user_research"
        assert artifact.bucket == Bucket.ELABORATE_DISCOVERY_AND_RESEARCH
        
        # Get non-existent artifact
        non_existent = project.get_artifact(Bucket.DEPLOY_SECURITY, "security_plan")
        assert non_existent is None
    
    def test_get_artifact_by_path(self, sample_project_memory):
        """Test retrieving artifacts by path"""
        project = sample_project_memory
        
        # Get existing artifact by path
        artifact = project.get_artifact_by_path(Path("docs/specs/api_specification.md"))
        assert artifact is not None
        assert artifact.name == "api_spec"
        assert artifact.bucket == Bucket.ELABORATE_SPECIFICATION_AND_PLANNING
        
        # Get non-existent artifact
        non_existent = project.get_artifact_by_path(Path("nonexistent/path.md"))
        assert non_existent is None
    
    def test_update_artifact_status(self, sample_project_memory):
        """Test updating artifact status"""
        project = sample_project_memory
        
        # Update existing artifact
        success = project.update_artifact_status(
            Bucket.INITIATE_INITIAL_PRODUCT_DOCS,
            "vision_doc",
            DocumentStatus.COMPLETED
        )
        assert success == True
        
        # Verify the status was updated
        artifact = project.get_artifact(Bucket.INITIATE_INITIAL_PRODUCT_DOCS, "vision_doc")
        assert artifact.status == DocumentStatus.COMPLETED
        
        # Try to update non-existent artifact
        success = project.update_artifact_status(
            Bucket.DEPLOY_SECURITY,
            "nonexistent",
            DocumentStatus.COMPLETED
        )
        assert success == False
    
    def test_get_bucket_completion(self, sample_project_memory):
        """Test getting completion stats for buckets"""
        project = sample_project_memory
        
        # Bucket with one completed artifact
        elaborate_spec_stats = project.get_bucket_completion(Bucket.ELABORATE_SPECIFICATION_AND_PLANNING)
        assert elaborate_spec_stats["total"] == 1
        assert elaborate_spec_stats["completed"] == 1
        assert elaborate_spec_stats["in_progress"] == 0
        assert elaborate_spec_stats["pending"] == 0
        assert elaborate_spec_stats["completion_percentage"] == 100.0
        
        # Bucket with one in-progress artifact
        elaborate_research_stats = project.get_bucket_completion(Bucket.ELABORATE_DISCOVERY_AND_RESEARCH)
        assert elaborate_research_stats["total"] == 1
        assert elaborate_research_stats["completed"] == 0
        assert elaborate_research_stats["in_progress"] == 1
        assert elaborate_research_stats["pending"] == 0
        assert elaborate_research_stats["completion_percentage"] == 0.0
        
        # Empty bucket
        empty_bucket_stats = project.get_bucket_completion(Bucket.DEPLOY_SECURITY)
        assert empty_bucket_stats["total"] == 0
        assert empty_bucket_stats["completion_percentage"] == 0.0
    
    def test_get_phase_completion(self, sample_project_memory):
        """Test getting completion stats for entire phases"""
        project = sample_project_memory
        
        # Elaborate phase has 2 artifacts (1 completed, 1 in-progress)
        elaborate_stats = project.get_phase_completion("Elaborate")
        assert elaborate_stats["total"] == 2
        assert elaborate_stats["completed"] == 1
        assert elaborate_stats["in_progress"] == 1
        assert elaborate_stats["pending"] == 0
        assert elaborate_stats["completion_percentage"] == 50.0
        assert elaborate_stats["buckets"] == 4  # All Elaborate buckets
        
        # Initiate phase has 1 pending artifact
        initiate_stats = project.get_phase_completion("Initiate")
        assert initiate_stats["total"] == 1
        assert initiate_stats["completed"] == 0
        assert initiate_stats["pending"] == 1
        assert initiate_stats["completion_percentage"] == 0.0
        
        # Deploy phase has 0 artifacts
        deploy_stats = project.get_phase_completion("Deploy")
        assert deploy_stats["total"] == 0
        assert deploy_stats["completion_percentage"] == 0.0
    
    def test_serialization_deserialization(self, sample_project_memory):
        """Test that ProjectMemory with taxonomy can be serialized and deserialized"""
        project = sample_project_memory
        
        # Serialize to JSON
        project_json = project.model_dump_json()
        
        # Check that JSON is valid
        parsed_json = json.loads(project_json)
        assert parsed_json["taxonomy_version"] == "0.5"
        assert "artifacts" in parsed_json
        
        # Deserialize from JSON
        loaded_project = ProjectMemory.model_validate_json(project_json)
        
        # Check that all fields were preserved
        assert loaded_project.metadata.name == project.metadata.name
        assert loaded_project.taxonomy_version == project.taxonomy_version
        
        # Check artifacts
        assert Bucket.INITIATE_INITIAL_PRODUCT_DOCS in loaded_project.artifacts
        assert Bucket.ELABORATE_DISCOVERY_AND_RESEARCH in loaded_project.artifacts
        assert Bucket.ELABORATE_SPECIFICATION_AND_PLANNING in loaded_project.artifacts
        
        # Check specific artifact fields were preserved
        loaded_artifact = loaded_project.get_artifact(Bucket.ELABORATE_SPECIFICATION_AND_PLANNING, "api_spec")
        assert loaded_artifact is not None
        assert loaded_artifact.name == "api_spec"
        assert loaded_artifact.path == Path("docs/specs/api_specification.md")
        assert loaded_artifact.status == DocumentStatus.COMPLETED
        
        # Verify completion stats are calculated correctly after deserialization
        stats = loaded_project.get_phase_completion("Elaborate")
        assert stats["completion_percentage"] == 50.0

================
File: tests/integration/test_server.py
================
#!/usr/bin/env python
"""
Integration tests for the Paelladoc MCP server.

These tests verify that the server correctly starts and responds to requests
via STDIO communication.
"""

import unittest
import sys
import os
import json
import time
import subprocess
import uuid
from pathlib import Path
# Removed pty/select imports as PTY test is skipped
import signal

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.parent
if str(project_root) not in sys.path:
    sys.path.insert(0, str(project_root))

# Constants
SERVER_SCRIPT = project_root / "server.py"

class TestServerIntegration(unittest.TestCase):
    """Integration tests for the MCP server STDIO communication."""
    
    @unittest.skip("Skipping PTY/STDIO test: FastMCP stdio interaction difficult to replicate reliably outside actual client environment.")
    def test_server_responds_to_ping(self):
        """Verify that the server responds to a ping request via PTY STDIO. (SKIPPED)"""
        request_id = str(uuid.uuid4())
        env = os.environ.copy()
        env["PYTHONPATH"] = str(project_root)
        env["PYTHONUNBUFFERED"] = "1"
        
        # --- Start server using PTY --- 
        # master_fd, slave_fd = pty.openpty() # PTY logic commented out
        server_process = None
        master_fd = None # Ensure master_fd is defined for finally block
        
        try:
            # server_process = subprocess.Popen(...)
            # os.close(slave_fd)
            
            # --- Test Communication --- 
            response_data = None
            stderr_output = ""
            
            # time.sleep(2) 
            
            # if server_process.poll() is not None:
            #     ...

            # mcp_request = {...}
            # request_json = json.dumps(mcp_request) + "\n"
            
            # print(f"Sending request via PTY: {request_json.strip()}")
            # os.write(master_fd, request_json.encode())
            
            # # Read response from PTY master fd with timeout
            # stdout_line = ""
            # buffer = b""
            # end_time = time.time() + 5 
            
            # while time.time() < end_time:
            #     ...
                         
            # print(f"Received raw response line: {stdout_line.strip()}")

            # if not stdout_line:
            #      ...

            # response_data = json.loads(stdout_line)
            # print(f"Parsed response: {response_data}")
            
            # self.assertEqual(...)
            pass # Keep test structure but do nothing as it's skipped

        except Exception as e:
            stderr_output = ""
            # ... (error handling commented out) ...
            self.fail(f"An error occurred during the PTY test (should be skipped): {e}")
            
        finally:
            # --- Cleanup --- 
            if master_fd:
                 try:
                      os.close(master_fd)
                 except OSError:
                      pass
            if server_process and server_process.poll() is None:
                print("Terminating server process (if it was started)...")
                try:
                    os.killpg(os.getpgid(server_process.pid), signal.SIGTERM)
                    server_process.wait(timeout=2)
                except (ProcessLookupError, subprocess.TimeoutExpired, AttributeError):
                    # Handle cases where process/pgid might not exist if startup failed early
                    print("Server cleanup notification: process termination might have failed or was not needed.")
                    if server_process and server_process.poll() is None:
                         try:
                              os.killpg(os.getpgid(server_process.pid), signal.SIGKILL)
                         except Exception:
                              pass # Final attempt
                except Exception as term_e:
                    print(f"Error during termination: {term_e}")
            # Read any remaining stderr
            if server_process and server_process.stderr:
                 stderr_rem = server_process.stderr.read().decode(errors='ignore')
                 if stderr_rem:
                      print(f"Remaining stderr: {stderr_rem}")


if __name__ == "__main__":
    unittest.main()

================
File: tests/integration/adapters/output/test_sqlite_memory_adapter.py
================
"""
Integration tests for the SQLiteMemoryAdapter.
"""

import unittest
import asyncio
import sys
import os
from pathlib import Path
import uuid
import datetime
import time # For potential delays if needed

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.parent.parent.absolute()
sys.path.insert(0, str(project_root))

# Module to test
from paelladoc.adapters.output.sqlite.sqlite_memory_adapter import SQLiteMemoryAdapter
from paelladoc.domain.models.project import ProjectMemory, ProjectMetadata, ProjectDocument, DocumentStatus

class TestSQLiteMemoryAdapterIntegration(unittest.IsolatedAsyncioTestCase):
    """Integration tests using a temporary SQLite DB."""

    async def asyncSetUp(self):
        """Set up a temporary database for each test."""
        # Generate unique path for this test run
        self.test_db_name = f"test_memory_{uuid.uuid4()}.db"
        self.test_db_path = Path("./temp_test_dbs") / self.test_db_name 
        self.test_db_path.parent.mkdir(parents=True, exist_ok=True)
        
        print(f"\nSetting up test with DB: {self.test_db_path}")
        self.adapter = SQLiteMemoryAdapter(db_path=self.test_db_path)
        # Ensure tables are created before tests run
        await self.adapter._create_db_and_tables()

    async def asyncTearDown(self):
        """Clean up the temporary database after each test."""
        print(f"Tearing down test, removing DB: {self.test_db_path}")
        # Explicitly close engine connections if possible/needed (aiosqlite might handle it)
        # await self.adapter.async_engine.dispose()
        # Give a tiny moment for the file lock to potentially release
        await asyncio.sleep(0.01) 
        try:
            if self.test_db_path.exists():
                os.remove(self.test_db_path)
                print(f"Removed DB: {self.test_db_path}")
            # Attempt to remove the directory if empty, fail silently if not
            try:
                self.test_db_path.parent.rmdir()
                print(f"Removed test directory: {self.test_db_path.parent}")
            except OSError:
                pass # Directory not empty, likely other tests running
        except Exception as e:
            print(f"Error during teardown removing {self.test_db_path}: {e}")
            
    def _create_sample_memory(self, name_suffix: str) -> ProjectMemory:
        """Helper to create a sample ProjectMemory object."""
        project_name = f"test-project-{name_suffix}"
        metadata = ProjectMetadata(
            name=project_name,
            language="python",
            purpose="testing adapter",
            target_audience="devs",
            objectives=["test save", "test load"]
        )
        documents = {
            "README.md": ProjectDocument(name="README.md", status=DocumentStatus.PENDING),
            "src/main.py": ProjectDocument(name="src/main.py", status=DocumentStatus.IN_PROGRESS, template_origin="template/python/main.md")
        }
        memory = ProjectMemory(metadata=metadata, documents=documents)
        return memory

    # --- Test Cases --- #

    async def test_project_exists_on_empty_db(self):
        """Test project_exists returns False when the DB is empty/project not saved."""
        print(f"Running: {self._testMethodName}")
        exists = await self.adapter.project_exists("nonexistent-project")
        self.assertFalse(exists)

    async def test_load_memory_on_empty_db(self):
        """Test load_memory returns None when the DB is empty/project not saved."""
        print(f"Running: {self._testMethodName}")
        loaded_memory = await self.adapter.load_memory("nonexistent-project")
        self.assertIsNone(loaded_memory)
        
    async def test_save_and_load_new_project(self):
        """Test saving a new project and loading it back."""
        print(f"Running: {self._testMethodName}")
        original_memory = self._create_sample_memory("save-load")
        project_name = original_memory.metadata.name
        
        # Save
        await self.adapter.save_memory(original_memory)
        print(f"Saved project: {project_name}")
        
        # Load
        loaded_memory = await self.adapter.load_memory(project_name)
        print(f"Loaded project: {project_name}")
        
        # Assertions
        self.assertIsNotNone(loaded_memory)
        self.assertEqual(loaded_memory.metadata.name, original_memory.metadata.name)
        self.assertEqual(loaded_memory.metadata.language, original_memory.metadata.language)
        self.assertEqual(loaded_memory.metadata.objectives, original_memory.metadata.objectives)
        self.assertEqual(len(loaded_memory.documents), len(original_memory.documents))
        
        # Check document details
        self.assertIn("README.md", loaded_memory.documents)
        self.assertEqual(loaded_memory.documents["README.md"].status, DocumentStatus.PENDING)
        self.assertIn("src/main.py", loaded_memory.documents)
        self.assertEqual(loaded_memory.documents["src/main.py"].status, DocumentStatus.IN_PROGRESS)
        self.assertEqual(loaded_memory.documents["src/main.py"].template_origin, "template/python/main.md")
        
        # Check timestamps (allow for slight differences in save/load)
        self.assertAlmostEqual(loaded_memory.created_at.timestamp(), original_memory.created_at.timestamp(), delta=1)
        self.assertAlmostEqual(loaded_memory.last_updated_at.timestamp(), original_memory.last_updated_at.timestamp(), delta=1)

    async def test_project_exists_after_save(self):
        """Test project_exists returns True after a project is saved."""
        print(f"Running: {self._testMethodName}")
        memory_to_save = self._create_sample_memory("exists")
        project_name = memory_to_save.metadata.name
        
        await self.adapter.save_memory(memory_to_save)
        print(f"Saved project: {project_name}")
        
        exists = await self.adapter.project_exists(project_name)
        self.assertTrue(exists)
        
    # Add more tests here for update, document add/remove, integrity errors etc.

# It's generally better to run tests using the unittest discovery mechanism,
# but this allows running the file directly.
# if __name__ == "__main__":
#     unittest.main()

================
File: tests/integration/adapters/output/test_chroma_vector_store_adapter.py
================
"""
Integration tests for the ChromaVectorStoreAdapter.
"""

import unittest
import asyncio
import sys
import os
from pathlib import Path
import uuid
import shutil # For cleaning up test directories

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.parent.parent.absolute()
sys.path.insert(0, str(project_root))

# Module to test
from paelladoc.adapters.output.chroma.chroma_vector_store_adapter import ChromaVectorStoreAdapter, NotFoundError
from paelladoc.ports.output.vector_store_port import SearchResult # Import base class

# Import Chroma specific types for assertions if needed
import chromadb
from chromadb.api.models.Collection import Collection


class TestChromaVectorStoreAdapterIntegration(unittest.IsolatedAsyncioTestCase):
    """Integration tests using an in-memory ChromaDB client."""

    def setUp(self):
        """Set up an in-memory Chroma client and a unique collection name."""
        print(f"\nSetting up test...")
        self.adapter = ChromaVectorStoreAdapter(in_memory=True)
        # Generate a unique collection name for each test to ensure isolation
        self.collection_name = f"test_collection_{uuid.uuid4()}"
        print(f"Using collection name: {self.collection_name}")

    async def asyncTearDown(self):
        """Attempt to clean up the test collection."""
        print(f"Tearing down test, attempting to delete collection: {self.collection_name}")
        try:
            # Use the adapter's method to delete
            await self.adapter.delete_collection(self.collection_name)
            print(f"Deleted collection: {self.collection_name}")
        except Exception as e:
            # Log error if deletion fails, but don't fail the test run
            print(f"Error during teardown deleting collection {self.collection_name}: {e}")
            # We can also try listing collections to see if it exists
            try:
                collections = self.adapter.client.list_collections()
                collection_names = [col.name for col in collections]
                if self.collection_name in collection_names:
                    print(f"Collection {self.collection_name} still exists after teardown attempt.")
                else:
                     print(f"Collection {self.collection_name} confirmed deleted or never existed.")
            except Exception as list_e:
                 print(f"Error listing collections during teardown check: {list_e}")

    # --- Test Cases --- #

    async def test_get_or_create_collection_creates_new(self):
        """Test that a new collection is created if it doesn't exist."""
        print(f"Running: {self._testMethodName}")
        collection = await self.adapter.get_or_create_collection(self.collection_name)
        self.assertIsInstance(collection, Collection)
        self.assertEqual(collection.name, self.collection_name)
        
        # Verify it exists in the client
        collections = self.adapter.client.list_collections()
        collection_names = [col.name for col in collections]
        self.assertIn(self.collection_name, collection_names)

    async def test_get_or_create_collection_retrieves_existing(self):
        """Test that an existing collection is retrieved."""
        print(f"Running: {self._testMethodName}")
        # Create it first
        collection1 = await self.adapter.get_or_create_collection(self.collection_name)
        self.assertIsNotNone(collection1)
        
        # Get it again
        collection2 = await self.adapter.get_or_create_collection(self.collection_name)
        self.assertIsInstance(collection2, Collection)
        self.assertEqual(collection2.name, self.collection_name)
        # Check they are likely the same underlying collection (same ID)
        self.assertEqual(collection1.id, collection2.id)

    async def test_add_documents(self):
        """Test adding documents to a collection."""
        print(f"Running: {self._testMethodName}")
        docs_to_add = ["doc one text", "doc two text"]
        metadatas = [{"source": "test1"}, {"source": "test2"}]
        ids = ["id1", "id2"]
        
        returned_ids = await self.adapter.add_documents(self.collection_name, docs_to_add, metadatas, ids)
        self.assertEqual(returned_ids, ids)
        
        # Verify documents were added using the underlying client API
        collection = await self.adapter.get_or_create_collection(self.collection_name)
        results = collection.get(ids=ids, include=["metadatas", "documents"])
        
        self.assertIsNotNone(results)
        self.assertListEqual(results['ids'], ids)
        self.assertListEqual(results['documents'], docs_to_add)
        self.assertListEqual(results['metadatas'], metadatas)
        self.assertEqual(collection.count(), 2)
        
    async def test_add_documents_without_ids(self):
        """Test adding documents letting Chroma generate IDs."""
        print(f"Running: {self._testMethodName}")
        docs_to_add = ["auto id doc 1", "auto id doc 2"]
        metadatas = [{"type": "auto"}, {"type": "auto"}]

        returned_ids = await self.adapter.add_documents(self.collection_name, docs_to_add, metadatas)
        
        self.assertEqual(len(returned_ids), 2)
        self.assertIsInstance(returned_ids[0], str)
        self.assertIsInstance(returned_ids[1], str)
        
        # Verify using the returned IDs
        collection = await self.adapter.get_or_create_collection(self.collection_name)
        results = collection.get(ids=returned_ids, include=["metadatas", "documents"])
        
        self.assertIsNotNone(results)
        self.assertCountEqual(results['ids'], returned_ids) # Order might not be guaranteed?
        self.assertCountEqual(results['documents'], docs_to_add)
        self.assertCountEqual(results['metadatas'], metadatas)
        self.assertEqual(collection.count(), 2)

    async def test_delete_collection(self):
        """Test deleting a collection."""
        print(f"Running: {self._testMethodName}")
        # Create it first
        await self.adapter.get_or_create_collection(self.collection_name)
        # Verify it exists
        collections_before = self.adapter.client.list_collections()
        self.assertIn(self.collection_name, [c.name for c in collections_before])
        
        # Delete it using the adapter
        await self.adapter.delete_collection(self.collection_name)
        
        # Verify it's gone
        collections_after = self.adapter.client.list_collections()
        self.assertNotIn(self.collection_name, [c.name for c in collections_after])
        
        # Attempting to get it should now raise NotFoundError or ValueError (depending on Chroma version)
        with self.assertRaises((NotFoundError, ValueError)):
            self.adapter.client.get_collection(name=self.collection_name)

    async def _add_sample_search_data(self):
        """Helper to add some consistent data for search tests."""
        docs = [
            "This is the first document about apples.",
            "This document discusses oranges and citrus.",
            "A third document, focusing on bananas.",
            "Another apple document for testing similarity."
        ]
        metadatas = [
            {"source": "doc1", "type": "fruit", "year": 2023},
            {"source": "doc2", "type": "fruit", "year": 2024},
            {"source": "doc3", "type": "fruit", "year": 2023},
            {"source": "doc4", "type": "fruit", "year": 2024}
        ]
        ids = ["s_id1", "s_id2", "s_id3", "s_id4"]
        await self.adapter.add_documents(self.collection_name, docs, metadatas, ids)
        print(f"Added sample search data to collection: {self.collection_name}")
        # Short delay to allow potential indexing if needed (though likely not for in-memory)
        await asyncio.sleep(0.1) 

    async def test_search_simple(self):
        """Test basic similarity search."""
        print(f"Running: {self._testMethodName}")
        await self._add_sample_search_data()
        
        query = "Tell me about apples"
        results = await self.adapter.search_similar(self.collection_name, [query], n_results=2)
        
        self.assertEqual(len(results), 1) # One list for the single query
        self.assertEqual(len(results[0]), 2) # Two results requested
        
        # Check the content of the results (order might vary based on embedding similarity)
        result_docs = [r.document for r in results[0]]
        self.assertIn("This is the first document about apples.", result_docs)
        self.assertIn("Another apple document for testing similarity.", result_docs)
        
        # Check metadata and ID are included
        first_result = results[0][0]
        self.assertIsInstance(first_result, SearchResult)
        self.assertIsNotNone(first_result.id)
        self.assertIsNotNone(first_result.metadata)
        self.assertIsNotNone(first_result.distance)

    async def test_search_with_metadata_filter(self):
        """Test search with a 'where' clause for metadata filtering."""
        print(f"Running: {self._testMethodName}")
        await self._add_sample_search_data()
        
        query = "Tell me about fruit"
        # Filter for documents from year 2023
        where_filter = {"year": 2023}
        results = await self.adapter.search_similar(self.collection_name, [query], n_results=3, where=where_filter)
        
        self.assertEqual(len(results), 1)
        # Should only find doc1 and doc3 from year 2023
        self.assertLessEqual(len(results[0]), 2) # Might return fewer than n_results if filter is strict
        
        returned_sources = [r.metadata.get("source") for r in results[0] if r.metadata]
        self.assertIn("doc1", returned_sources)
        self.assertIn("doc3", returned_sources)
        self.assertNotIn("doc2", returned_sources)
        self.assertNotIn("doc4", returned_sources)

    async def test_search_no_results(self):
        """Test search for text unrelated to the documents."""
        print(f"Running: {self._testMethodName}")
        await self._add_sample_search_data()
        
        query = "Information about programming languages"
        results = await self.adapter.search_similar(self.collection_name, [query], n_results=1)
        
        self.assertEqual(len(results), 1)
        # Depending on the embedding model, might still return *something* even if very dissimilar.
        # A more robust test might check the distance if available.
        # For now, let's assume it might return the closest, even if irrelevant, or empty.
        # If it returns results, ensure they are SearchResult instances
        if results[0]:
            self.assertIsInstance(results[0][0], SearchResult)
        else:
            self.assertEqual(len(results[0]), 0) # Or assert empty list

    async def test_search_in_nonexistent_collection(self):
        """Test search returns empty list if collection doesn't exist."""
        print(f"Running: {self._testMethodName}")
        query = "anything"
        results = await self.adapter.search_similar("nonexistent_collection_for_search", [query], n_results=1)
        
        self.assertEqual(len(results), 1) # Still returns a list for the query
        self.assertEqual(len(results[0]), 0) # But the inner list is empty

# if __name__ == "__main__":
#     unittest.main()

================
File: tests/e2e/test_cursor_simulation.py
================
"""
End-to-End tests for Paelladoc MCP Server.

This simulates how Cursor would interact with the server.
"""

import unittest
import sys
import os
from pathlib import Path

# Ensure we can import Paelladoc modules
project_root = Path(__file__).parent.parent.parent.absolute()
if str(project_root) not in sys.path:
    sys.path.insert(0, str(project_root))

# Import directly from the domain layer
from paelladoc.domain.core_logic import mcp, ping

class TestCursorE2E(unittest.TestCase):
    """End-to-End tests simulating Cursor interacting with Paelladoc."""
    
    def test_direct_ping_call(self):
        """Test direct call to the ping function."""
        # Call the ping function directly
        result = ping()
        
        # Verify the result
        self.assertIsInstance(result, dict, "Ping should return a dict")
        self.assertEqual(result["status"], "ok", "Status should be 'ok'")
        self.assertEqual(result["message"], "pong", "Message should be 'pong'")
    
    def test_ping_with_parameter(self):
        """Test ping function with a parameter."""
        # Call ping with a test parameter
        result = ping(random_string="test-parameter")
        
        # Verify the result
        self.assertIsInstance(result, dict, "Ping should return a dict")
        self.assertEqual(result["status"], "ok", "Status should be 'ok'")
        self.assertEqual(result["message"], "pong", "Message should be 'pong'")
    
    def test_mcp_tool_registration(self):
        """Verify that the ping tool is registered with MCP."""
        # Get tools registered with MCP
        tool_manager = getattr(mcp, "_tool_manager", None)
        self.assertIsNotNone(tool_manager, "MCP should have a tool manager")
        
        tools = tool_manager.list_tools()
        
        # Check if the ping tool is registered
        tool_names = [tool.name for tool in tools]
        self.assertIn("ping", tool_names, "Ping tool should be registered with MCP")

if __name__ == "__main__":
    unittest.main()
