# Design Document: Task Name Column ## Overview This feature adds the `name__c` database column to the Task model and all task-related queries, enabling users to see and look up tasks by their short human-readable name. The changes span four layers: 1. **Model** – Add an optional `name` field to the `Task` dataclass. 2. **Database** – Modify SQL queries in `TrinoClient` to SELECT `name__c`; add a new `get_task_by_name` method. 3. **Protocol** – Extend the `DBClient` protocol with `get_task_by_name`. 4. **MCP Tools** – Include the name in list/detail output; resolve name-based lookups in `get_task_details`. ## Architecture ```mermaid flowchart TD A[MCP Tool: list_open_tasks] -->|calls| B[TrinoClient.get_open_tasks] C[MCP Tool: get_task_details] -->|calls| D[TrinoClient.get_task_by_id] C -->|calls| E[TrinoClient.get_task_by_name] B --> F[(Trino DB: name__c)] D --> F E --> F B --> G[Task dataclass with name field] D --> G E --> G ``` The identifier resolution logic in `get_task_details` will attempt `get_task_by_id` first. If that returns `None`, it falls back to `get_task_by_name`. This keeps backward compatibility for callers passing IDs while enabling name-based lookup without a separate tool. ## Components and Interfaces ### 1. Task Dataclass (`models.py`) Add a single field: ```python @dataclass(frozen=True) class Task: id: str name: Optional[str] # NEW – maps to name__c title: str description: str start_date: Optional[date] end_date: Optional[date] created_date: datetime skills: list[str] = field(default_factory=list) ``` The field is `Optional[str]` because the database column can be NULL. ### 2. DBClient Protocol (`database/types.py`) Add one method: ```python def get_task_by_name(self, name: str) -> Task | None: """Fetch one published task by its short name (name__c).""" raise NotImplementedError ``` ### 3. TrinoClient (`database/trino_client.py`) **SQL changes** – Every SELECT that builds a `Task` must include `t.name__c`. Affected methods: - `get_open_tasks` – add `t.name__c` to both the unlimited and CTE-limited queries. - `get_task_by_id` – add `t.name__c` to the SELECT. **New method** – `get_task_by_name`: ```python def get_task_by_name(self, name: str) -> Optional[Task]: """Fetch a single published task by name__c (case-sensitive).""" query = """ SELECT t.id, t.name__c, t.title__c, t.description__c, t.startdate__c, t.enddate__c, t.createddate, s.skillname__c FROM beschaffungstool_kmp_task_latest t LEFT JOIN beschaffungstool_kmp_skill_latest s ON t.id = s.task__c WHERE t.name__c = ? AND t.status__c = ? """ # Same row-assembly logic as get_task_by_id ``` ### 4. MCP Server (`mcp_server.py`) **`list_open_tasks`** – Add a "Name" column to the output table (between task_id and Title). **`get_task_details`** – Change the resolution logic: ```python @mcp.tool() async def get_task_details(task_id: str) -> str: # 1. Try by ID task = db_client.get_task_by_id(task_id) # 2. Fallback: try by name if task is None: task = db_client.get_task_by_name(task_id) # 3. Not found if task is None: return f"Task not found or not published: {task_id}" # ... rest unchanged, but include task.name in the summary table ``` Add "Name" to the summary table header and row. ## Data Models ### Task Table Schema (relevant columns) | Column | Type | Nullable | Maps to | |--------|------|----------|---------| | `id` | VARCHAR | No | `Task.id` | | `name__c` | VARCHAR | Yes | `Task.name` | | `title__c` | VARCHAR | Yes | `Task.title` | | `description__c` | VARCHAR | Yes | `Task.description` | | `startdate__c` | DATE | Yes | `Task.start_date` | | `enddate__c` | DATE | Yes | `Task.end_date` | | `createddate` | TIMESTAMP | No | `Task.created_date` | ### Task Dataclass (after change) ```python @dataclass(frozen=True) class Task: id: str name: Optional[str] title: str description: str start_date: Optional[date] end_date: Optional[date] created_date: datetime skills: list[str] = field(default_factory=list) ``` ## Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* ### Property 1: Task name field preserves database value *For any* database row with a `name__c` value (including NULL), when the row is mapped to a `Task` object, `Task.name` must equal the original `name__c` value (with NULL mapped to `None`). **Validates: Requirements 1.1, 1.2, 1.3, 1.4** ### Property 2: Task output contains name *For any* `Task` with a non-None `name` field, the formatted output string (from both `list_open_tasks` and `get_task_details`) must contain that name value as a substring. **Validates: Requirements 2.1, 2.2** ### Property 3: Name lookup returns correct task *For any* task that exists in the database with a given `name__c` value, calling `get_task_by_name` with that exact name must return a `Task` whose `name` field equals the queried name. **Validates: Requirements 3.2** ### Property 4: Name resolution equivalence *For any* published task with a non-None name, calling `get_task_details` with the task's name must produce the same task data as calling `get_task_details` with the task's ID. **Validates: Requirements 3.4** ### Property 5: Unknown identifier returns not-found *For any* string that is neither a valid published task ID nor a valid published task name, `get_task_details` must return a not-found message. **Validates: Requirements 3.3, 3.5** ## Error Handling | Scenario | Behavior | |----------|----------| | `name__c` is NULL in DB | `Task.name` is set to `None`; output displays empty or omits the name cell | | `get_task_by_name` receives empty string | Returns `None` (no match) | | `get_task_details` identifier matches neither ID nor name | Returns `"Task not found or not published: {identifier}"` | | Multiple tasks share the same `name__c` (data quality issue) | `get_task_by_name` returns the first match (ORDER BY createddate DESC) | The resolution order in `get_task_details` (ID first, then name) ensures that if a name happens to look like an ID, the ID lookup takes precedence. This avoids ambiguity. ## Testing Strategy ### Unit Tests - Verify `Task` dataclass construction with `name=None` and `name="ABC-123"`. - Verify `list_open_tasks` output table includes a "Name" column. - Verify `get_task_details` output table includes the name value. - Verify `get_task_details` falls back to `get_task_by_name` when ID lookup returns None. - Verify `get_task_by_name` returns None for empty string input. ### Property-Based Tests Use **Hypothesis** (Python PBT library) with a minimum of 100 iterations per property. Each test must be tagged with a comment referencing the design property: ```python # Feature: task-name-column, Property 1: Task name field preserves database value ``` **Property 1** – Generate random `(name__c_value | None)` values, construct Task objects via the row-mapping helper, assert `task.name == input_value`. **Property 2** – Generate random Task objects with non-None names (using `st.text(min_size=1)`), format them through the output functions, assert the name string appears in the output. **Property 3** – Generate a random set of tasks with unique names, mock the DB cursor to return those rows, call `get_task_by_name(name)`, assert the returned task's name matches. **Property 4** – Generate a task with a non-None name, mock both `get_task_by_id` and `get_task_by_name` to return the same task, call `get_task_details` with the name and with the ID, assert both outputs are identical. **Property 5** – Generate random strings, filter out any that match known task IDs or names in the mock data, call `get_task_details`, assert the output contains "not found". ### Test Configuration ```python from hypothesis import given, settings, strategies as st @settings(max_examples=100) ```