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.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
# 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)
|
||||
```
|
||||
Reference in New Issue
Block a user