Files
Orchestrator/bahn/teamlandkarte-mcp/.kiro/specs/task-name-column/design.md
T
ankn a5f8fb49ab Migrate all repos into monorepo context folders
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.
2026-06-30 20:39:52 +02:00

223 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```