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
+245
View File
@@ -0,0 +1,245 @@
# filepath: tests/test_bm25.py
"""Unit tests for :mod:`teamlandkarte_mcp.matching.bm25`.
Covers tokenization, :class:`Bm25Index` behaviour, and the
:func:`bm25_rank_competences` convenience wrapper.
"""
from __future__ import annotations
from teamlandkarte_mcp.matching.bm25 import (
Bm25Index,
_tokenize,
bm25_rank_competences,
)
# ---------------------------------------------------------------------------
# Tokenization
# ---------------------------------------------------------------------------
def test_tokenize_lowercase() -> None:
"""Tokens are always lowercase."""
assert _tokenize("Python") == ["python"]
def test_tokenize_slash() -> None:
"""Slashes split tokens."""
assert _tokenize("CI/CD Pipeline") == ["ci", "cd", "pipeline"]
def test_tokenize_parentheses_and_space() -> None:
"""Parentheses and spaces split tokens; PWA is its own token."""
assert _tokenize("Progressive Web App (PWA)") == [
"progressive",
"web",
"app",
"pwa",
]
def test_tokenize_dot() -> None:
"""Dots split tokens (e.g. React.js → react, js)."""
assert _tokenize("React.js") == ["react", "js"]
def test_tokenize_hyphen() -> None:
"""Hyphens split tokens."""
assert _tokenize("Type-Script") == ["type", "script"]
def test_tokenize_empty_string() -> None:
"""Empty string produces empty token list."""
assert _tokenize("") == []
def test_tokenize_only_special_chars() -> None:
"""String of only special characters produces empty list."""
assert _tokenize("---///") == []
# ---------------------------------------------------------------------------
# Bm25Index — empty corpus
# ---------------------------------------------------------------------------
def test_bm25_index_empty_corpus_rank_returns_empty() -> None:
"""Empty corpus → rank() returns empty list."""
index = Bm25Index(corpus=[])
assert index.rank("Python") == []
# ---------------------------------------------------------------------------
# Bm25Index — exact match
# ---------------------------------------------------------------------------
def test_bm25_index_exact_match_score_positive() -> None:
"""Exact match produces a positive BM25 score in a realistic-sized corpus.
A corpus of five distinct skills ensures each term's IDF is positive
(no term appears in more than half the documents), so BM25 assigns a
meaningful positive score to the matching document.
"""
corpus = ["Python", "JavaScript", "Java", "Go", "Ruby"]
index = Bm25Index(corpus=corpus)
result = index.rank("Python")
py_score = next(s for d, s in result if d == "Python")
assert py_score > 0.0
def test_bm25_index_case_insensitive_match() -> None:
"""BM25 matching is case-insensitive (tokenization lowercases both sides).
Uses a realistic-sized corpus so IDF weights are positive.
"""
corpus = ["python", "JavaScript", "Java", "Go", "Ruby"]
index = Bm25Index(corpus=corpus)
result = index.rank("Python")
py_score = next(s for d, s in result if d == "python")
assert py_score > 0.0
# ---------------------------------------------------------------------------
# Bm25Index — no token overlap
# ---------------------------------------------------------------------------
def test_bm25_index_no_token_overlap_score_zero() -> None:
"""No shared token between query and document → score 0.0."""
index = Bm25Index(corpus=["JavaScript"])
result = index.rank("Python")
assert len(result) == 1
assert result[0][0] == "JavaScript"
assert result[0][1] == 0.0
def test_bm25_index_partial_token_overlap() -> None:
"""Partial token overlap gives positive score in a realistic-sized corpus.
"Machine Learning" shares the "learning" token with "Deep Learning".
A five-document corpus keeps "learning" IDF positive (it appears in 2/5
docs, well below the 50 % threshold that would zero out the IDF).
"""
corpus = ["Deep Learning", "Python", "JavaScript", "Java", "Go"]
index = Bm25Index(corpus=corpus)
result = index.rank("Machine Learning")
dl_score = next(s for d, s in result if d == "Deep Learning")
assert dl_score > 0.0
# ---------------------------------------------------------------------------
# Bm25Index — multiple documents, ranking order
# ---------------------------------------------------------------------------
def test_bm25_index_best_match_ranked_first() -> None:
"""The document with the highest score appears first.
Uses five documents so all query tokens have positive IDF and the
exact-match document outscores partial-match documents.
"""
corpus = ["Machine Learning", "Deep Learning", "JavaScript", "Python", "Java"]
index = Bm25Index(corpus=corpus)
result = index.rank("Machine Learning")
assert result[0][0] == "Machine Learning"
assert result[0][1] > result[1][1]
def test_bm25_index_zero_score_documents_included() -> None:
"""Documents with score 0.0 are included in the ranked list.
Uses a realistic-sized corpus so the Python document itself gets a
positive score, while the unrelated JavaScript document correctly
scores 0.0 (no token overlap).
"""
corpus = ["Python", "JavaScript", "Java", "Go", "Ruby"]
index = Bm25Index(corpus=corpus)
result = index.rank("Python")
docs = [r[0] for r in result]
assert "JavaScript" in docs
js_score = next(s for d, s in result if d == "JavaScript")
assert js_score == 0.0
def test_bm25_index_result_sorted_descending() -> None:
"""Ranked list is sorted by score descending.
Uses a realistic-sized corpus to ensure stable positive IDF values.
"""
corpus = ["Python", "Python Advanced", "JavaScript", "Java", "Go"]
index = Bm25Index(corpus=corpus)
result = index.rank("Python")
scores = [s for _, s in result]
assert scores == sorted(scores, reverse=True)
# ---------------------------------------------------------------------------
# bm25_rank_competences convenience wrapper
# ---------------------------------------------------------------------------
def test_bm25_rank_competences_empty_candidates() -> None:
"""Empty candidate list returns empty list."""
assert bm25_rank_competences("Python", []) == []
def test_bm25_rank_competences_consistent_with_index() -> None:
"""Convenience wrapper produces same result as Bm25Index.rank()."""
candidates = ["Python", "JavaScript", "Machine Learning", "Java", "Go"]
index_result = Bm25Index(corpus=candidates).rank("Python")
wrapper_result = bm25_rank_competences("Python", candidates)
assert index_result == wrapper_result
# ---------------------------------------------------------------------------
# Global-index filtering
# ---------------------------------------------------------------------------
def test_bm25_index_rank_filters_to_candidate_subset() -> None:
"""Scores from a global index can be filtered to a candidate's own skills.
This mirrors the production path in ``_bm25_rrf_similarity`` where a
global ``Bm25Index`` is built over all candidates and then the result is
filtered to only include the current candidate's competences.
"""
global_corpus = [
"Python",
"Machine Learning",
"JavaScript",
"TypeScript",
"Java",
"Go",
"Ruby",
]
global_index = Bm25Index(corpus=global_corpus)
# Simulate one candidate who only knows Python + Machine Learning
candidate_skills = {"Python", "Machine Learning"}
all_ranked = global_index.rank("Python")
filtered = [(doc, score) for doc, score in all_ranked if doc in candidate_skills]
assert len(filtered) == 2
docs = {d for d, _ in filtered}
assert docs == candidate_skills
# Python should score higher than Machine Learning for query "Python"
py_score = next(s for d, s in filtered if d == "Python")
ml_score = next(s for d, s in filtered if d == "Machine Learning")
assert py_score > ml_score
def test_bm25_ubiquitous_term_clamped_to_zero() -> None:
"""A term that appears in more than half the corpus gets IDF ≤ 0.
``rank()`` clamps the resulting negative BM25 score to 0.0 — the term
is non-discriminative and should not contribute positively to any match.
"""
# "dev" appears in 4 out of 5 documents → IDF < 0
corpus = ["dev Python", "dev Java", "dev Go", "dev Ruby", "JavaScript"]
index = Bm25Index(corpus=corpus)
result = index.rank("dev")
scores = [s for _, s in result]
# All scores must be ≥ 0 (negative raw scores clamped to 0.0)
assert all(s >= 0.0 for s in scores)