Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
223 lines
8.1 KiB
Markdown
223 lines
8.1 KiB
Markdown
# 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)
|
||
```
|