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,311 @@
from __future__ import annotations
from datetime import date
from unittest.mock import AsyncMock, MagicMock
import pytest
from teamlandkarte_mcp.config import MatchingConfig, MatchingThresholds
from teamlandkarte_mcp.matching.matcher import Matcher
from teamlandkarte_mcp.matching.similarity import SimilarityEngine
from teamlandkarte_mcp.models import Capacity, Requirements
class _FakeSimilarityEngine:
def __init__(
self,
*,
comp_score: float = 1.0,
role_score: float = 0.0,
):
self._comp_score = comp_score
self._role_score = role_score
@property
def use_bm25_search(self) -> bool:
"""Always False — the fake engine never builds a global index."""
return False
async def compute_competence_similarity( # noqa: ANN001
self,
required,
_candidate,
global_index=None, # noqa: ANN001
):
# Return per required competence entries.
return {
r: {"score": self._comp_score, "best_match": None, "rationale": ""}
for r in required
}
async def compute_role_similarity( # noqa: ANN001
self,
_required_role,
_candidate_role,
):
return self._role_score
@pytest.mark.asyncio
async def test_matcher_uses_similarity_engine_scores() -> None:
cfg = MatchingConfig(
competence_weight=0.8,
role_weight=0.2,
thresholds=MatchingThresholds(top=0.8, good=0.6, partial=0.4),
require_confirmation=True,
)
matcher = Matcher(_FakeSimilarityEngine(), cfg) # type: ignore[arg-type]
caps = [
Capacity(
id=1,
owner_name="A",
role_name="Dev",
role_level=None,
begin_date=date(2025, 1, 1),
end_date=date(2025, 12, 31),
competences=["Python"],
)
]
req = Requirements(
role_name="Dev",
competences=["Python"],
date_start=None,
date_end=None,
description=None,
)
result = await matcher.match(caps, req)
assert len(result.scored) == 1
assert result.scored[0].competence_score == 1.0
assert result.scored[0].overall_score == 0.8
assert result.scored[0].category == "Top"
# ---------------------------------------------------------------------------
# BM25 integration: false-positive elimination
# ---------------------------------------------------------------------------
def _cfg() -> MatchingConfig:
return MatchingConfig(
competence_weight=0.8,
role_weight=0.2,
thresholds=MatchingThresholds(top=0.8, good=0.6, partial=0.4),
require_confirmation=False,
)
def _async_client() -> MagicMock:
"""Mock AzureOpenAIClient."""
client = MagicMock()
client.chat_completion = AsyncMock(return_value='{"similarity": 0.0}')
return client
def _bm25_engine() -> SimilarityEngine:
"""Minimal SimilarityEngine with BM25 competence similarity."""
return SimilarityEngine(
client=_async_client(), # type: ignore[arg-type]
)
@pytest.mark.asyncio
async def test_bm25_candidate_with_exact_skills_scores_higher() -> None:
"""BM25: candidate with exact skills scores higher than JS-only candidate."""
engine = _bm25_engine()
matcher = Matcher(engine, _cfg())
caps = [
Capacity(
id=1,
owner_name="Alice",
role_name="ML Engineer",
role_level=None,
begin_date=None,
end_date=None,
competences=["Python", "Machine Learning", "Pandas"],
),
Capacity(
id=2,
owner_name="Bob",
role_name="Frontend Dev",
role_level=None,
begin_date=None,
end_date=None,
competences=["JavaScript", "TypeScript", "Node.js", "Vue.js"],
),
]
req = Requirements(
role_name="ML Engineer",
competences=["Python", "Machine Learning"],
date_start=None,
date_end=None,
description=None,
)
result = await matcher.match(caps, req)
scored = {s.capacity.id: s for s in result.scored}
# Alice has exact matches → competence_score > 0
assert scored[1].competence_score > 0.0
# Bob has zero token overlap → competence_score == 0.0 (false-positive eliminated)
assert scored[2].competence_score == 0.0
# Alice scores higher overall
assert scored[1].overall_score > scored[2].overall_score
@pytest.mark.asyncio
async def test_bm25_false_positive_eliminated() -> None:
"""BM25: JS/TS candidate gets 0.0 when Python/ML is required."""
engine = _bm25_engine()
matcher = Matcher(engine, _cfg())
caps = [
Capacity(
id=3,
owner_name="Carol",
role_name="Frontend Dev",
role_level=None,
begin_date=None,
end_date=None,
competences=["JavaScript", "TypeScript", "Node.js", "Vue.js",
"PWA", "CI/CD"],
),
]
req = Requirements(
role_name="Data Scientist",
competences=["Python", "Machine Learning"],
date_start=None,
date_end=None,
description=None,
)
result = await matcher.match(caps, req)
assert result.scored[0].competence_score == 0.0
@pytest.mark.asyncio
async def test_bm25_auto_tag_candidate_matches_after_expansion() -> None:
"""BM25 + auto-tag: 'ML' candidate matches 'Machine Learning' via mock tagger.
A second candidate "Eve" holds "Machine Learning" as an original skill so
it enters the global BM25 corpus. After the mock tagger expands Dave's
competences to include "Machine Learning", the global index can score it
with positive IDF (N=5 unique skills in the pool).
"""
mock_tagger = MagicMock()
mock_tagger.expand_competences = AsyncMock(
return_value=["ML", "Python", "Machine Learning"]
)
engine = SimilarityEngine(
client=_async_client(), # type: ignore[arg-type]
use_auto_tagging=True,
auto_tagger=mock_tagger,
)
matcher = Matcher(engine, _cfg())
caps = [
Capacity(
id=4,
owner_name="Dave",
role_name="Data Scientist",
role_level=None,
begin_date=None,
end_date=None,
competences=["ML", "Python"],
),
# Eve's original skills make "Machine Learning" part of the global
# BM25 corpus, giving it a stable positive IDF weight.
Capacity(
id=5,
owner_name="Eve",
role_name="ML Engineer",
role_level=None,
begin_date=None,
end_date=None,
competences=["Machine Learning", "Data Science", "Statistics"],
),
]
req = Requirements(
role_name="Data Scientist",
competences=["Machine Learning", "Python"],
date_start=None,
date_end=None,
description=None,
)
result = await matcher.match(caps, req)
dave = next(s for s in result.scored if s.capacity.id == 4)
# After auto-tag expansion "Machine Learning" is in Dave's working list
# and in the global corpus → competence_score > 0
assert dave.competence_score > 0.0
# ---------------------------------------------------------------------------
# Global BM25 index construction
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_matcher_builds_global_bm25_index_across_all_candidates() -> None:
"""Matcher builds one global BM25 index from all filtered candidates' skills.
With a global corpus of seven distinct skills the IDF weights are stable
and each candidate is scored against the *same* index. Two candidates
with non-overlapping skill sets must receive different competence scores:
the one whose skills share tokens with the required competence scores
above zero, the other must score exactly zero.
This verifies the fix for the per-person IDF pathology: if a separate
index were built per-person the candidate with only one skill would get
IDF ≤ 0 and score 0.0 incorrectly.
"""
engine = _bm25_engine()
matcher = Matcher(engine, _cfg())
caps = [
# Frank: single relevant skill — previously broken by per-person N=1
Capacity(
id=10,
owner_name="Frank",
role_name="Backend Dev",
role_level=None,
begin_date=None,
end_date=None,
competences=["Python"],
),
# Grace: unrelated skills only
Capacity(
id=11,
owner_name="Grace",
role_name="Designer",
role_level=None,
begin_date=None,
end_date=None,
competences=["Figma", "Sketch", "CSS", "HTML", "Illustrator", "InDesign"],
),
]
req = Requirements(
role_name="Backend Dev",
competences=["Python"],
date_start=None,
date_end=None,
description=None,
)
result = await matcher.match(caps, req)
scored = {s.capacity.id: s for s in result.scored}
# With a global corpus of 7 unique skills, IDF("python") > 0.
# Frank's single-skill corpus would have given IDF < 0 (per-person bug).
assert scored[10].competence_score > 0.0, (
"Frank should score > 0: global IDF fixes the per-person N=1 pathology"
)
# Grace has no token overlap with "Python" → score exactly 0.0
assert scored[11].competence_score == 0.0
# Frank ranks above Grace
assert scored[10].overall_score > scored[11].overall_score