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,134 @@
# Feature: llm-fulltext-matching, Property 8: Ungekürzte Rationale wird persistiert
"""Property-based test verifying that long rationales are persisted verbatim.
The MCP server stores LLM matcher results into ``SearchCache`` by merging
the source ``Capacity``/``Task`` payload with ``category`` and
``rationale`` fields from each :class:`LlmFulltextItem` (see
``mcp_server.find_matching_capacities``). The persistence step is a
simple pass-through:
merged = dict(it.raw)
merged["rationale"] = it.rationale
That means the LLM-returned rationale on the
:class:`~teamlandkarte_mcp.matching.llm_fulltext_matcher.LlmFulltextItem`
must already be byte-for-byte identical to the LLM payload. This test
exercises the matcher directly with a fake LLM that echoes long
rationales (>= 300 chars) and asserts the resulting item carries the
same string back.
Testing at the matcher level is sufficient evidence for requirement 8.6:
the cache pass-through cannot introduce truncation if the source item
already holds the unmodified text. The table-rendering helper
``_format_rationale_for_table`` is covered separately by the Property 7
test (``test_format_rationale_pbt.py``).
**Validates: Requirements 8.6**
"""
from __future__ import annotations
import asyncio
import json
from typing import Any
from hypothesis import given, settings
from hypothesis import strategies as st
from teamlandkarte_mcp.matching.llm_fulltext_matcher import LlmFulltextMatcher
from teamlandkarte_mcp.matching.profiles import TaskProfile
from teamlandkarte_mcp.models import Capacity
# Exclude characters that would be mangled by JSON serialization or that
# Hypothesis would otherwise generate (lone surrogates, control chars,
# line/paragraph separators). The remaining alphabet is wide enough to
# exercise multi-byte text while staying round-trippable through
# ``json.dumps``/``json.loads``.
_RATIONALE_STRATEGY = st.text(
alphabet=st.characters(
blacklist_categories=("Cs", "Cc", "Zl", "Zp"),
),
min_size=300,
max_size=400,
)
class _StubDb:
"""Minimal ``DBClient`` double returning empty extras for every id."""
def batch_get_capacity_descriptions(
self, capacity_ids: list[Any]
) -> dict[str, str | None]:
return {str(i): None for i in capacity_ids}
def batch_get_capacity_certificates(
self, capacity_ids: list[Any]
) -> dict[str, list[str]]:
return {str(i): [] for i in capacity_ids}
def batch_get_capacity_references(
self, capacity_ids: list[Any]
) -> dict[str, list[dict]]:
return {str(i): [] for i in capacity_ids}
class _EchoLlm:
"""Returns the supplied rationale wrapped in a ``Top`` JSON payload."""
def __init__(self, rationale: str) -> None:
self._rationale = rationale
async def chat_completion(self, system: str, user: str) -> str:
return json.dumps(
{"category": "Top", "rationale": self._rationale},
ensure_ascii=False,
)
def _capacity() -> Capacity:
return Capacity(
id=1,
owner_name="o",
role_name="R",
role_level=None,
begin_date=None,
end_date=None,
competences=[],
)
@settings(max_examples=100, deadline=None)
@given(rationale=_RATIONALE_STRATEGY)
def test_long_rationale_is_persisted_unchanged(rationale: str) -> None:
"""Property 8: matcher returns the rationale verbatim, length >= 300.
The matcher's per-item ``rationale`` is what the MCP server merges
into the persisted SearchCache payload, so equality at this layer
proves the rationale survives the pass-through unchanged.
"""
matcher = LlmFulltextMatcher(
db=_StubDb(),
client=_EchoLlm(rationale),
)
task_profile = TaskProfile(id="t", title="T", description="D", skills=[])
result = asyncio.run(
matcher.match_capacities(
task_profile=task_profile,
capacities=[_capacity()],
)
)
assert result.errors == [], f"unexpected errors: {result.errors}"
items = result.by_category["Top"]
assert len(items) == 1, "expected exactly one Top item"
item = items[0]
assert item.category == "Top"
# The rationale must be bit-identical to what the LLM returned, even
# though the input is well above the 280-char display limit.
assert item.rationale == rationale, (
"rationale was modified between LLM response and matcher item"
)
assert len(item.rationale) >= 300