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:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,384 @@
# Implementation Plan: LLM-Volltext-Matching als zweites Verfahren
## Overview
Inkrementelle Einführung des neuen `llm_fulltext`-Matching-Verfahrens neben dem bestehenden `score`-Verfahren. Die Reihenfolge stellt sicher, dass keine Zwischenstände kaputt sind: zuerst Datenbankschicht (DBClient/TrinoClient) erweitern, dann Datenmodelle (`CapacityProfile`/`TaskProfile`) und der `LlmFulltextMatcher`, anschließend Konfiguration und MCP-Tool-Integration, am Ende Cache-/Tabellenausgabe sowie Anpassungen an `get_results_by_category`/`filter_search_results` und Dokumentation.
## Tasks
- [x] 1. Datenbankschicht für Capacity-Volltext-Felder erweitern
- [x] 1.1 Neue Methoden im `DBClient`-Protokoll deklarieren (`database/types.py`)
- `CapacityReferenceRow` als `TypedDict` mit Feldern `partner_name: str` und `projects: str` im Protokoll-Modul (bzw. unter `database/types.py`) deklarieren; `partner_name` darf leer sein (NULL `partner_id` oder Join-Mismatch, Anforderung 2.8)
- `get_capacity_description(capacity_id) -> str | None`
- `get_capacity_certificates(capacity_id) -> list[str]`
- `get_capacity_references(capacity_id) -> list[CapacityReferenceRow]`
- `batch_get_capacity_descriptions(capacity_ids) -> dict[str, str | None]`
- `batch_get_capacity_certificates(capacity_ids) -> dict[str, list[str]]`
- `batch_get_capacity_references(capacity_ids) -> dict[str, list[CapacityReferenceRow]]`
- Docstrings gemäß Design (Quelle, Join-Verhalten inkl. Partner-Join `partner_id = id`, Rückgabetyp bei leerer Eingabe, leerer `partner_name` bei NULL/Mismatch)
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.8_
- [x] 1.2 Einzel-Methoden im `TrinoClient` implementieren (`database/trino_client.py`)
- SELECT-Only-Statements mit `_ensure_select_only`, Connection-Pool und `_retry`
- `get_capacity_description`: NULL/leerer String → `None`
- `get_capacity_certificates`: Join über `capacity_id`, leere Strings filtern, Rückgabe `[]` wenn keine Treffer
- `get_capacity_references`: zusätzlich `LEFT JOIN teamlandkarte_v_partners_latest p ON r.partner_id = p.id` und `COALESCE(p.name, '') AS partner_name` in derselben Abfrage; Rückgabe als `CapacityReferenceRow`-Liste mit `partner_name` und `projects`
- NULL `partner_id` bzw. Join-Mismatch → leerer `partner_name`, Referenz bleibt mit `projects` erhalten (Anforderung 2.8)
- Parameter-Bindung gegen SQL-Injection
- _Requirements: 2.1, 2.2, 2.3, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10_
- [x] 1.3 Batch-Methoden im `TrinoClient` implementieren
- Genau **eine** SQL-Abfrage pro Quelle (Anforderung 2.4); der Partner-LEFT-JOIN ist Bestandteil derselben Referenz-Abfrage und erzeugt keinen zusätzlichen Roundtrip
- `batch_get_capacity_references`: `SELECT r.capacity_id, r.projects, COALESCE(p.name, '') AS partner_name ... LEFT JOIN teamlandkarte_v_partners_latest p ON r.partner_id = p.id`
- Schlüssel der Rückgaben sind `str(capacity_id)` (deterministisch)
- Fehlende IDs als `None` (Beschreibungen) bzw. `[]` (Listen) vorbelegen
- Gruppierung n:1 via `defaultdict(list)`, leere `projects`-Strings filtern; leerer `partner_name` (NULL/Mismatch) führt nicht zum Verwerfen der Referenz
- Frühe Rückkehr bei leerer Eingabeliste (`{}`)
- _Requirements: 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10_
- [x] 1.4 Unit-Tests für die neuen DB-Methoden mit Mock-Cursor*
- Genau eine SQL-Abfrage je Methode wird abgesetzt; LEFT JOIN auf `teamlandkarte_v_partners_latest` ist Bestandteil derselben Referenz-Abfrage (kein zusätzlicher Roundtrip)
- SELECT-Only-Guard wird angewendet
- Korrekte Gruppierung n:1 in den Batch-Varianten
- Mock-Cursor liefert für `references` drei Spalten (`capacity_id`, `projects`, `partner_name`); Test inkl. Fall NULL `partner_id` → leerer `partner_name`, Referenz bleibt erhalten
- Defaults für fehlende IDs (None / [])
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8_
- [x] 2. Datenmodelle `CapacityProfile` und `TaskProfile`
- [x] 2.1 Neue frozen Dataclasses anlegen (`matching/profiles.py`)
- `CapacityReferenceEntry` als `@dataclass(frozen=True)` mit Feldern `partner_name: str` und `projects: str` (`partner_name` darf leer sein, vgl. Anforderung 2.8 / 3.2)
- `CapacityProfile` mit Feldern `id`, `owner_name`, `role_name`, `competences`, `description`, `references: list[CapacityReferenceEntry]`, `certificates`
- `TaskProfile` mit Feldern `id`, `title`, `description`, `skills`
- Beide `@dataclass(frozen=True)` und nur primitive Felder bzw. `list[str]` / `list[CapacityReferenceEntry]`
- _Requirements: 3.1, 3.2, 4.1_
- [x] 2.2 Profil-Builder implementieren (`matching/profiles.py`)
- `build_capacity_profile(capacity, *, description, certificates, references) -> CapacityProfile`
- Konvertiere `CapacityReferenceRow`-Einträge aus dem DBClient in `CapacityReferenceEntry`-Instanzen; ein leerer `partner_name` führt nicht zum Verwerfen der Referenz (Anforderung 3.4)
- `build_task_profile(task) -> TaskProfile` (alternativ `build_task_profile_from_requirements(...)` für Suchrichtung Aufgabe→Kapazität)
- Leere/`None`-Felder → leerer String bzw. leere Liste, Profil wird nie verworfen
- Reihenfolge der Felder ist über alle Profile konstant
- _Requirements: 3.1, 3.2, 3.4, 4.1, 4.2_
- [x] 2.3 Deterministische Profil-Serialisierung implementieren
- `serialize_capacity_profile(profile) -> str` mit fixer Reihenfolge `Rolle:`, `Kompetenzen:`, `Beschreibung:`, `Referenzen:`, `Zertifikate:`
- `_format_reference(entry: CapacityReferenceEntry) -> str`: bei nicht-leerem `partner_name``Partner: <partner_name> Projekte: <projects>`; bei leerem `partner_name``Projekte: <projects>` (kein Platzhalter, kein `Partner:`-Token)
- `serialize_task_profile(profile) -> str` mit fixer Reihenfolge `Titel:`, `Beschreibung:`, `Gesuchte Kompetenzen:`
- Leere Listen werden als `(keine)` ausgegeben, alle Feldüberschriften erscheinen immer; Reihenfolge der Felder und Referenzen bleibt deterministisch
- _Requirements: 3.3, 3.4, 3.6, 3.7, 4.3, 4.4_
- [x] 2.4 Property-Test für deterministische Serialisierung schreiben
- **Property 1: Profil-Serialisierung ist deterministisch und feldvollständig**
- Hypothesis mit `@settings(max_examples=100)`, `st.builds(...)` für Profile inkl. `CapacityReferenceEntry` mit/ohne `partner_name`
- Zwei Aufrufe liefern identischen String; alle Feldüberschriften enthalten; Partner-Name erscheint deterministisch in der serialisierten Ausgabe, sofern nicht leer
- **Validates: Requirements 3.3, 3.4, 3.6, 3.7, 4.3, 4.4**
- [x] 2.5 Property-Test für Profil-Builder mit leeren Feldern schreiben
- **Property 2: Leere/None-Felder verwerfen das Profil nicht**
- Capacity-/Task-Strategien mit `st.one_of(st.none(), st.text())`; Referenzen-Strategie mit gemischtem `partner_name` (`st.one_of(st.just(""), st.text(min_size=1))`)
- Profil enthält leere Strings/Listen; Serialisierung enthält weiterhin alle Überschriften; auch Referenzen mit leerem `partner_name` werden nicht verworfen
- **Validates: Requirements 3.2, 3.4, 4.2**
- [x] 2.6 Property-Test für Referenzen mit leerem Partner-Name schreiben
- **Property 2b: Referenzen mit leerem Partner_Name behalten projects, ohne Partner-Token**
- Hypothesis-Generator mischt leere und nicht-leere `partner_name`-Werte: `st.lists(st.builds(CapacityReferenceEntry, partner_name=st.one_of(st.just(""), st.text(min_size=1)), projects=st.text(min_size=1)))`
- Prüft (a) die Anzahl der Referenz-Zeilen entspricht der Anzahl der Einträge mit nicht-leerem `projects`, (b) jede Zeile zu einem Eintrag mit leerem `partner_name` beginnt mit `Projekte:` und enthält keinen `Partner:`-Token, (c) jede Zeile zu einem Eintrag mit nicht-leerem `partner_name` enthält sowohl `Partner: <name>` als auch `Projekte: <projects>`
- **Validates: Requirements 2.8, 3.4, 3.6**
- [x] 3. `LlmFulltextMatcher`-Komponente bauen
- [x] 3.1 Modul-Skelett `matching/llm_fulltext_matcher.py` anlegen
- Konstanten `_ALLOWED_CATEGORIES = ("Top", "Good", "Partial", "Low", "Irrelevant")` und `_ALIAS`
- Dataclasses `LlmFulltextItem`, `LlmFulltextError`, `LlmFulltextResult`
- Klasse `LlmFulltextMatcher` mit Konstruktor `(*, db, client, rationale_max_chars=280)`
- _Requirements: 5.1, 5.3, 6.1, 6.3, 7.4_
- [x] 3.2 Kategorie-Normalisierung implementieren
- `normalize_category(value) -> tuple[str, bool]`
- Trim + lowercase, Mapping über `_ALIAS`, ungültige Werte → `("Irrelevant", False)`
- Nicht-String-Eingaben → `("Irrelevant", False)`
- _Requirements: 5.3, 5.6, 6.3, 6.6_
- [x] 3.3 Property-Test für Kategorie-Normalisierung schreiben
- **Property 3: Kategorienormalisierung bildet auf erlaubte Menge ab**
- Hypothesis mit `st.text()` und Aliase via `st.sampled_from`
- Output immer in erlaubter Menge; `is_valid` korrekt
- **Validates: Requirements 5.3, 5.6, 6.3, 6.6**
- [x] 3.4 LLM-Aufruf und Antwort-Parsing implementieren
- System-Prompt gemäß Design (deutschsprachig, deterministisch, JSON-only)
- User-Prompt baut auf `serialize_task_profile`/`serialize_capacity_profile` auf
- `chat_completion(system, user, response_format=json_object)` aufrufen
- JSON parsen, Felder `category` und `rationale` extrahieren
- Bei `JSONDecodeError``LlmFulltextError` mit Meldung `"invalid JSON: <excerpt>"`
- Bei ungültiger Kategorie → Item nach `Irrelevant` mit Hinweis `[Hinweis: ungültige LLM-Kategorie: <wert>]` an Rationale anhängen
- Bei `AzureAPIError`/Timeout/sonstiger Exception → `LlmFulltextError` mit Klassenname + erstem Satz
- _Requirements: 5.4, 5.5, 5.6, 5.7, 6.4, 6.5, 6.6, 6.7_
- [x] 3.5 `match_capacities` implementieren
- Eingabe: `task_profile`, `capacities` (bereits vorgefiltert)
- Capacity-IDs sammeln, `batch_get_capacity_descriptions/certificates/references` aufrufen
- Pro Kapazität `CapacityProfile` bauen und LLM-Aufruf durchführen
- Erfolgreiche Items in `by_category` einsortieren, fehlerhafte in `errors`
- Innerhalb jeder Kategorie deterministisch nach `item_id` aufsteigend (lexikographisch) sortieren
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.7, 5.8_
- [x] 3.6 `match_tasks` implementieren
- Eingabe: `capacity_profile`, `tasks`
- Pro Aufgabe `TaskProfile` bauen und LLM-Aufruf durchführen
- Sortierung primär nach Kategorie, sekundär nach `task_id` aufsteigend
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.7, 6.8_
- [x] 3.7 Property-Test für ungültige LLM-Kategorie schreiben
- **Property 4: Ungültige LLM-Kategorie wird auf Irrelevant gemappt**
- Mock-LLM gibt zufällige ungültige Kategorie zurück
- Item landet in `Irrelevant`, Rationale enthält Originaltext + Hinweis
- **Validates: Requirements 5.6, 6.6**
- [x] 3.8 Property-Test für LLM-Fehler-Trennung schreiben
- **Property 5: LLM-Fehler erscheinen in der Fehlerliste, nicht als Ergebnis**
- Hypothesis-Strategy `st.lists(st.booleans())` als Fehlermaske
- Items aus Fehlermaske erscheinen ausschließlich in `result.errors`
- **Validates: Requirements 5.7, 6.7**
- [x] 3.9 Property-Test für deterministische Sortierung schreiben
- **Property 6: Ergebnisse sind innerhalb jeder Kategorie deterministisch sortiert**
- Permutationen der Eingabeliste erzeugen identische `by_category`-Reihenfolge
- **Validates: Requirements 5.8, 6.8**
- [x] 3.10 Unit-Tests für `LlmFulltextMatcher` mit gemocktem `AzureOpenAIClient`*
- Erfolgsfall (gültige Kategorie + Rationale)
- Ungültiges JSON
- Gültiges JSON mit unbekannter Kategorie
- `chat_completion` wirft `AzureAPIError`
- Vermischung mehrerer Items: Reihenfolge und Sortierung korrekt
- _Requirements: 5.4, 5.5, 5.6, 5.7, 5.8, 6.4, 6.5, 6.6, 6.7, 6.8_
- [x] 4. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 5. Konfiguration `matching.default_method` einführen
- [x] 5.1 `MatchingConfig` um Feld `default_method: str = "score"` erweitern (`config.py`)
- Validierung in `load_config`: Wert muss `"score"` oder `"llm_fulltext"` sein
- Bei ungültigem Wert `ConfigError` mit beschreibender Meldung werfen
- Fehlender Schlüssel → Default `"score"`
- _Requirements: 12.2, 12.3, 12.4_
- [x] 5.2 `config.toml` und `config.toml.example` aktualisieren
- Neuer Abschnitt `[matching].default_method = "score"` mit Kommentar zu erlaubten Werten
- _Requirements: 12.2, 12.3_
- [x] 5.3 Unit-Tests für Konfigurations-Validierung*
- `default_method` fehlt → Default `"score"`
- `default_method = "llm_fulltext"` → übernommen
- `default_method = "irgendwas"``ConfigError`
- _Requirements: 12.3, 12.4_
- [x] 6. MCP-Tool-Integration für `matching_method`
- [x] 6.1 Hilfsfunktion `_resolve_matching_method` im `mcp_server.py` implementieren
- `None``cfg.matching.default_method`
- Trim + lowercase, gegen `("score", "llm_fulltext")` validieren
- Bei ungültigem Wert `ValueError` mit beiden erlaubten Werten in der Meldung werfen
- _Requirements: 1.1, 1.2, 1.5, 12.3_
- [x] 6.2 `find_matching_capacities` um `matching_method` erweitern
- Neuer Parameter `matching_method: Optional[str] = None`
- Validierung **vor** DB-/LLM-Aufrufen, bei Fehler frühe Markdown-Fehlermeldung mit erlaubten Werten zurückgeben
- Routing: `"score"` → bestehender `Matcher`-Pfad; `"llm_fulltext"``LlmFulltextMatcher.match_capacities`
- Im LLM-Modus Vorfilter (z. B. Verfügbarkeit) identisch zum Score-Modus anwenden
- Task_Profile aus aktuell bestätigtem Anforderungs-Set bauen
- _Requirements: 1.1, 1.3, 1.4, 1.5, 5.1, 5.2_
- [x] 6.3 `find_matching_tasks` um `matching_method` erweitern
- Neuer Parameter `matching_method: Optional[str] = None`
- Routing analog 6.2
- Im LLM-Modus für die `capacity_id` Beschreibung, Zertifikate und Referenzen aus DB laden und ins `CapacityProfile` einbeziehen
- _Requirements: 1.1, 1.3, 1.4, 1.5, 6.1, 6.2_
- [x] 6.4 Docstrings für betroffene MCP-Tools aktualisieren
- `find_matching_capacities`, `find_matching_tasks`: Parameter `matching_method` mit erlaubten Werten dokumentieren
- Hinweis auf Unterschiede in Ausgabe-Schema (keine Score-Spalten im LLM-Modus, `Begründung`-Spalte)
- _Requirements: 12.1_
- [x] 6.5 Property-Test für `matching_method`-Validierung schreiben
- **Property 9: matching_method-Validierung lehnt unbekannte Werte ab**
- Hypothesis-Strategy `st.text()`
- Bei ungültiger Eingabe enthält die Fehlermeldung beide erlaubten Werte; DB- und LLM-Mocks werden nicht aufgerufen
- **Validates: Requirements 1.5**
- [x] 6.6 Unit-Tests für Routing in beiden MCP-Tools
- `matching_method=None` → Default greift, Schema entspricht Score-Modus
- `matching_method="llm_fulltext"``LlmFulltextMatcher` wird aufgerufen, Score-Pfad nicht
- `matching_method="bogus"` → Fehlermeldung, weder DB noch LLM werden aufgerufen
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 7. SearchCache-Payload und META-JSON
- [x] 7.1 Persistenz im SearchCache an LLM-Modus anpassen
- `matching_method` als Schlüssel im Payload aufnehmen (auch im Score-Modus)
- Im LLM-Modus pro Item Felder `category` und `rationale` (ungekürzt) speichern, **keine** `role_score`/`competence_score`/`overall_score`
- Optionale `errors`-Liste mit `{"item_id", "error"}` im LLM-Modus
- Im Score-Modus bisheriges Schema unverändert (keine `rationale`/`errors`)
- _Requirements: 7.1, 7.4, 8.6, 9.5_
- [x] 7.2 META-JSON in Tool-Antworten um `matching_method` erweitern
- In beiden Modi setzen, sodass Folgewerkzeuge das Schema korrekt interpretieren
- _Requirements: 1.6, 9.5_
- [x] 7.3 Property-Test für persistierte ungekürzte Rationale schreiben
- **Property 8: Ungekürzte Rationale wird persistiert**
- Hypothesis `st.text(min_size=300)`, Cache-Eintrag muss exakt der LLM-Antwort entsprechen
- **Validates: Requirements 8.6**
- [x] 7.4 Property-Test für META-`matching_method` schreiben
- **Property 11: META enthält das verwendete Verfahren**
- Aus Tool-Output META-JSON parsen und Wert prüfen
- **Validates: Requirements 1.6, 9.5**
- [x] 7.5 Property-Test für Score-Modus-Abwärtskompatibilität schreiben
- **Property 10: Score-Modus ist abwärtskompatibel**
- Tool-Aufruf ohne `matching_method` bzw. mit `"score"`: Persistiertes Payload-Schema entspricht weiter dem bisherigen (Felder `role_score`, `competence_score`, `overall_score`; kein `rationale`)
- **Validates: Requirements 1.2, 1.3, 7.3**
- [x] 8. Tabellenausgabe mit `Begründung`-Spalte und Rationale-Formatierung
- [x] 8.1 Hilfsfunktion `_format_rationale_for_table` implementieren
- Pipes (`|`) → `/`, Carriage Return / Linefeed → Leerzeichen
- Whitespace zusammenfalten
- Bei > 280 Zeichen kürzen und mit `…` abschließen
- _Requirements: 8.4, 8.5_
- [x] 8.2 Ausgabe-Tabellen für `find_matching_capacities` (LLM-Modus) anpassen
- Spalten: `ID | Owner | Role | Competences | Availability | Category | Begründung`
- **Keine** Spalten `Role Score`, `Competence Score`, `Overall Score`
- Pro Zeile gekürzte Rationale via `_format_rationale_for_table`
- Leere Ergebnistabelle weiterhin korrekt rendern (Header-Konsistenz)
- _Requirements: 7.1, 7.2, 8.1, 8.2, 8.3, 8.4, 8.5_
- [x] 8.3 Ausgabe-Tabellen für `find_matching_tasks` (LLM-Modus) anpassen
- Spalten: `task_id | Title | Required Competences | Availability | Category | Begründung`
- Verhalten analog 8.2
- _Requirements: 7.1, 7.2, 8.1, 8.2, 8.3, 8.4, 8.5_
- [x] 8.4 Summary-Tabelle und Errors-Block einbauen
- Summary bleibt in beiden Modi identisch (Counter je Kategorie)
- Wenn `errors` nicht leer: zusätzlicher Markdown-Block `## Errors` nach der Ergebnistabelle
- _Requirements: 5.7, 6.7, 7.5_
- [x] 8.5 Score-Modus-Tabelle unverändert lassen (Regression)
- Bestehende Spalten `Role Score`, `Competence Score`, `Overall Score`, `Category` bleiben
- Keine `Begründung`-Spalte
- _Requirements: 7.3_
- [x] 8.6 Property-Test für `_format_rationale_for_table` schreiben
- **Property 7: Tabellen-Rationale ist gültiges Markdown und längenbegrenzt**
- Hypothesis erzeugt Strings inkl. `|`, `\n`, sehr lang
- Output ≤ 280 Zeichen, weder `|` noch Zeilenumbrüche, `…`-Suffix genau dann, wenn normalisierte Eingabe > 280 Zeichen war
- **Validates: Requirements 8.4, 8.5**
- [x] 9. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 10. Anpassungen für `get_results_by_category`
- [x] 10.1 Hilfsfunktion `_format_results_table(items, *, search_type, matching_method, ref_start, ref_end)` einführen
- Routing nach `matching_method`: Score-Tabelle (bisher) oder LLM-Tabelle mit `Begründung`-Spalte
- Wiederverwendbar in `find_matching_capacities`, `find_matching_tasks` und `get_results_by_category`
- _Requirements: 7.1, 7.2, 8.1, 8.2, 9.2_
- [x] 10.2 `get_results_by_category` an LLM-Modus anpassen
- `matching_method` aus persistiertem SearchEntry lesen
- Im LLM-Modus Tabelle ohne Score-Spalten und mit `Begründung`-Spalte rendern
- Im Score-Modus unverändertes Verhalten
- META-JSON enthält `matching_method`
- _Requirements: 9.1, 9.2, 9.5_
- [x] 10.3 Docstring für `get_results_by_category` aktualisieren
- Hinweis auf modusabhängige Spalten (`Begründung` im LLM-Modus)
- _Requirements: 12.1_
- [x] 11. Anpassungen für `filter_search_results`
- [x] 11.1 Modus-Erkennung über persistiertes `matching_method`
- Im LLM-Modus bestehende Filter (Rollen-, Kompetenz-, Verfügbarkeits-, Aufgaben-Text-/Kompetenzfilter) weiterhin anwenden
- Score-bezogenen Filter `min_similarity` ignorieren und in `Applied Filters` einen Hinweis aufnehmen (`min_similarity (ignored: not applicable in llm_fulltext mode)`)
- _Requirements: 9.3, 9.4_
- [x] 11.2 Sortierung in `filter_search_results` modusabhängig
- Score-Modus: bestehende Sortierung nach `overall_score`
- LLM-Modus: stabile Sortierung nach `(category_rank, item_id)`
- _Requirements: 5.8, 6.8, 9.3_
- [x] 11.3 Tabellen-Rendering in `filter_search_results` an `_format_results_table` anbinden
- Verwendung der neuen Hilfsfunktion (siehe 10.1) für konsistente Spalten
- META-JSON enthält `matching_method`
- _Requirements: 9.2, 9.5_
- [x] 11.4 Docstring für `filter_search_results` aktualisieren
- Hinweis, dass `min_similarity` im LLM-Modus ignoriert wird
- _Requirements: 12.1_
- [x] 11.5 Unit-Tests für `get_results_by_category` und `filter_search_results` in beiden Modi
- LLM-Modus: `Begründung`-Spalte vorhanden, `min_similarity` ignoriert mit Hinweis
- Score-Modus: bestehendes Verhalten unverändert (Regression)
- _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5_
- [x] 12. Server-Wiring und Integrationstests
- [x] 12.1 `LlmFulltextMatcher` in `build_server` instanziieren
- Konstruktor mit `db`, `client` (`AzureOpenAIClient`) verdrahten
- Komponente an MCP-Tool-Handler weiterreichen
- _Requirements: 1.4, 5.1, 6.1_
- [x] 12.2 End-to-End-Integrationstests mit gemocktem LLM-Client und gemocktem `DBClient`
- `find_matching_capacities` und `find_matching_tasks` mit `matching_method="llm_fulltext"`
- Verfügbarkeitsfilter im LLM-Modus identisch zum Score-Modus
- SearchCache enthält `matching_method`, ungekürzte `rationale`, `errors`-Liste
- _Requirements: 5.1, 5.2, 6.1, 6.2, 7.4, 8.6, 9.5_
- [x] 12.3 Tabellen-Snapshot-Test für Header in beiden Modi
- Score-Modus-Header unverändert
- LLM-Modus-Header endet auf `... | Category | Begründung`
- _Requirements: 7.1, 8.1, 8.2_
- [x] 13. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 14. Dokumentation aktualisieren
- [x] 14.1 `docs/architecture.md` erweitern
- `LLM_Fulltext_Matcher` als Komponente in der Business-Logic-Layer beschreiben (Eingaben, Ausgaben, externe Abhängigkeit Azure OpenAI Chat Completion)
- Zusätzliche Datenquellen (`teamlandkarte_v_capacities_latest.description`, `teamlandkarte_v_capacity_certificates_latest`, `teamlandkarte_v_capacity_references_latest`, `teamlandkarte_v_partners_latest.{id, name}`) im Schema-Verifikationsabschnitt aufführen
- Join `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id` und Übernahme der Spalte `name` als Partner_Name dokumentieren
- Tool-Surface-Tabelle um Parameter `matching_method` (Wertebereich `score`/`llm_fulltext`) ergänzen
- Runtime-View für beide Suchrichtungen um den LLM-Volltext-Pfad erweitern
- _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_
- [x] 14.2 `README.md` erweitern
- Quick-Start- und Usage-Abschnitt: Auswahl zwischen `score` und `llm_fulltext` per Tool-Parameter
- Hinweis: Im LLM-Volltext-Modus keine numerischen Scores; stattdessen Spalte `Begründung`
- Zusätzliche Datenbank-Views auflisten, die der Server im LLM-Volltext-Modus liest, einschließlich `teamlandkarte_v_partners_latest` mit Hinweis auf den LEFT JOIN über `partner_id` in der Referenz-Abfrage
- Default-Konfiguration `[matching].default_method`
- _Requirements: 11.6, 11.7, 11.8_
- [x] 14.3 `.github/agents/teamlandkarte_agent.md` aktualisieren
- Beschreibung beider Verfahren `score` und `llm_fulltext` (Existenz und Zweck)
- Pflicht-Frage nach `matching_method` vor `find_matching_capacities`/`find_matching_tasks`, sofern nicht aus dem Verlauf bekannt
- Skills/Workflows: Aufruf der Tools mit zusätzlichem Parameter `matching_method`
- Rolle der Spalte `Begründung` und Hinweis, dass im LLM-Modus keine numerischen Scores erscheinen
- Bestehender Bestätigungs-Workflow (`show_pending_requirements`, `confirm_requirements`) bleibt für beide Verfahren gleich
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
- [x] 14.4 `.kiro/agents/teamlandkarte.md` analog zu 14.3 aktualisieren
- Inhaltliche Gleichheit zum GitHub-Pendant sicherstellen
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
- [x] 14.5 Beispiele und Mini-Walkthroughs in README/Architektur ergänzen
- Beispielausgabe einer LLM-Volltext-Tabelle inkl. Spalte `Begründung`
- Beispielhafte META-JSON-Ausgabe mit `matching_method`
- _Requirements: 11.5, 11.6_
- [x] 15. Final checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks markiert mit `*` sind optional und können für einen schnelleren MVP übersprungen werden.
- Property-Based Tests verwenden Hypothesis mit `@settings(max_examples=100)` (mindestens 100 Iterationen pro Property) und werden als `# Feature: llm-fulltext-matching, Property {N}: {title}` getaggt.
- Unit- und Integrationstests verwenden `pytest` (Run-once, kein Watch-Modus); der `AzureOpenAIClient` wird stets gemockt.
- Tasks referenzieren explizit Anforderungen aus `requirements.md` zur lückenlosen Nachverfolgbarkeit.
- Die Implementierungssprache ist Python (bestehende Codebase).
- Score-Modus bleibt vollständig abwärtskompatibel; nur additive Erweiterungen am Payload und am META-JSON.