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,344 @@
# Feature: team-profile-matching, Property 6: Determinismus und Vollständigkeit der Team-Serialisierung
"""Property-based tests for deterministic team-profile serialization.
Covers Property 6 from the team-profile-matching design:
For every :class:`TeamProfile` instance ``p`` (including instances with all
mandatory fields empty, empty competence/reference lists, and references
with empty ``partner_name``) the helpers ``serialize_team_profile`` and
``build_team_profile`` from
``teamlandkarte_mcp.matching.profiles`` must satisfy:
1. ``serialize_team_profile(p) == serialize_team_profile(p)``
(determinism, requirement 13.1).
2. ``serialize_team_profile(build_team_profile(team)) ==
serialize_team_profile(build_team_profile(team))`` for every ``Team``
(idempotency of profile construction, requirement 13.2).
3. The output contains every one of the seven headings ``Teamname:``,
``Schwerpunkt:``, ``Über uns:``, ``Leistungen:``, ``Interessen:``,
``Kompetenzen:``, ``Referenzen:`` at least once and exactly in this
order (requirements 5.4, 5.7, 13.3).
4. For every competence with ``top_competency=True`` the output contains
the marker suffix ``(Top)`` directly attached to the competence name
(requirement 5.5).
5. For every reference with ``partner_name != ""`` the output contains
both the ``partner_name`` and the ``projects`` text as substrings
(requirement 5.6); for references with ``partner_name == ""`` the
corresponding line does **not** contain the ``Partner: `` token
(requirement 5.3).
6. The order of competence and reference list entries in the
serialization matches the order of ``profile.competences`` and
``profile.references`` (requirement 13.4).
**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 13.1, 13.2, 13.3, 13.4**
"""
from __future__ import annotations
from hypothesis import given, settings
from hypothesis import strategies as st
from teamlandkarte_mcp.matching.profiles import (
TeamCompetenceEntry,
TeamProfile,
TeamReferenceEntry,
build_team_profile,
serialize_team_profile,
)
from teamlandkarte_mcp.models import Team, TeamCompetence, TeamReference
# ---------------------------------------------------------------------------
# Hypothesis strategies
# ---------------------------------------------------------------------------
# Small text strategy to keep generation fast and deterministic. Allows the
# empty string so the property covers the "all mandatory fields empty"
# case explicitly mentioned in the design.
_text = st.text(max_size=40)
# Competence names must be non-whitespace so substring assertions (e.g.
# ``f"{name} (Top)" in output``) are unambiguous.
_competence_name = st.text(min_size=1, max_size=30).filter(lambda s: s.strip() != "" and "\n" not in s)
# ``partner_name`` may be empty (NULL ``partner_id`` / Join mismatch) or
# any short non-empty text without newlines so substring checks remain
# unambiguous.
_partner_name = st.one_of(
st.just(""),
st.text(min_size=1, max_size=20).filter(lambda s: s.strip() != "" and "\n" not in s),
)
# The DB layer has filtered whitespace-only ``projects``, so every
# generated reference uses a non-empty, non-whitespace value. We also
# avoid newlines so per-reference line containment checks are robust.
_projects = st.text(min_size=1, max_size=40).filter(lambda s: s.strip() != "" and "\n" not in s)
_team_competence_entry = st.builds(
TeamCompetenceEntry,
name=_competence_name,
top_competency=st.booleans(),
)
_team_reference_entry = st.builds(
TeamReferenceEntry,
partner_name=_partner_name,
projects=_projects,
)
@st.composite
def _team_profile(draw: st.DrawFn) -> TeamProfile:
"""Generate a ``TeamProfile`` with mixed empty/non-empty fields and lists."""
return TeamProfile(
id=draw(_text),
ouid=draw(_text),
team_name=draw(_text),
focus_name=draw(_text),
about_us=draw(_text),
offerings=draw(_text),
interests=draw(_text),
competences=draw(st.lists(_team_competence_entry, max_size=5)),
references=draw(st.lists(_team_reference_entry, max_size=5)),
)
_team_competence_model = st.builds(
TeamCompetence,
name=_competence_name,
top_competency=st.booleans(),
)
_team_reference_model = st.builds(
TeamReference,
partner_name=_partner_name,
projects=_projects,
)
@st.composite
def _team(draw: st.DrawFn) -> Team:
"""Generate a ``Team`` with mixed empty/non-empty fields and lists."""
return Team(
team_id=draw(_text),
ouid=draw(_text),
team_name=draw(_text),
focus_name=draw(_text),
about_us=draw(_text),
offerings=draw(_text),
interests=draw(_text),
competences=draw(st.lists(_team_competence_model, max_size=5)),
references=draw(st.lists(_team_reference_model, max_size=5)),
)
_HEADINGS = (
"Teamname:",
"Schwerpunkt:",
"Über uns:",
"Leistungen:",
"Interessen:",
"Kompetenzen:",
"Referenzen:",
)
# ---------------------------------------------------------------------------
# Properties
# ---------------------------------------------------------------------------
@settings(max_examples=100, deadline=None)
@given(profile=_team_profile())
def test_serialize_team_profile_is_deterministic(profile: TeamProfile) -> None:
"""Property 6.1: ``serialize_team_profile`` is deterministic.
Two consecutive calls on the same profile must yield byte-identical
strings (requirement 13.1).
"""
first = serialize_team_profile(profile)
second = serialize_team_profile(profile)
assert first == second, "serialize_team_profile is not deterministic"
@settings(max_examples=100, deadline=None)
@given(team=_team())
def test_serialize_team_profile_is_idempotent_via_build(team: Team) -> None:
"""Property 6.2: ``build_team_profile`` is idempotent under serialization.
For every ``Team``, building a profile and serializing it must yield
the exact same string on repeated invocations (requirement 13.2).
"""
first = serialize_team_profile(build_team_profile(team))
second = serialize_team_profile(build_team_profile(team))
assert first == second, "serialize_team_profile(build_team_profile(team)) is not idempotent"
@settings(max_examples=100, deadline=None)
@given(profile=_team_profile())
def test_serialize_team_profile_contains_all_headings_in_order(
profile: TeamProfile,
) -> None:
"""Property 6.3: Every heading appears at least once and in fixed order.
The serialized output must contain each of the seven headings
``Teamname:``, ``Schwerpunkt:``, ``Über uns:``, ``Leistungen:``,
``Interessen:``, ``Kompetenzen:``, ``Referenzen:`` and these
headings must appear strictly in this order
(requirements 5.4, 5.7, 13.3).
"""
output = serialize_team_profile(profile)
# Headings are emitted by the serializer at the start of a line.
# We require each heading to occur as a line prefix and search
# strictly past the previously-found heading line so user-provided
# text containing the same heading substring (e.g.
# ``interests='Referenzen:'``) cannot perturb the ordering check.
lines = output.split("\n")
last_line_index = -1
for heading in _HEADINGS:
found_line = -1
for i in range(last_line_index + 1, len(lines)):
if lines[i].startswith(heading):
found_line = i
break
assert found_line != -1, (
f"Heading {heading!r} missing at or after line "
f"{last_line_index + 1} in team serialization: "
f"{output!r}"
)
last_line_index = found_line
@settings(max_examples=100, deadline=None)
@given(profile=_team_profile())
def test_serialize_team_profile_marks_top_competences(
profile: TeamProfile,
) -> None:
"""Property 6.4: Top competences are marked with ``(Top)``.
For every competence with ``top_competency=True`` the serialized
output must contain the substring ``"<name> (Top)"`` (requirement
5.5).
"""
output = serialize_team_profile(profile)
for competence in profile.competences:
if competence.top_competency:
marker = f"{competence.name} (Top)"
assert marker in output, (
f"Top-competence marker {marker!r} missing from serialization: {output!r}"
)
@settings(max_examples=100, deadline=None)
@given(profile=_team_profile())
def test_serialize_team_profile_partner_handling(
profile: TeamProfile,
) -> None:
"""Property 6.5: Partner-name visibility and absence handling.
For every reference with non-empty ``partner_name`` the serialized
output contains both the partner name and the projects text as a
substring (requirement 5.6). For every reference with empty
``partner_name`` the corresponding line does not contain the
``"Partner: "`` token (requirement 5.3).
"""
output = serialize_team_profile(profile)
# Locate the references block; entries are emitted as ``- <line>``
# bullets directly after the ``Referenzen:`` heading line. The
# serializer separates lines exclusively with ``\n``; ``str.splitlines``
# would also split on Unicode separators (e.g. U+0085) that may legally
# appear inside ``projects``, so we deliberately use ``split("\n")``.
# ``Referenzen:`` is always preceded by ``Interessen: ...\n`` in the
# serializer's fixed field order, so the leading-newline anchor is
# guaranteed to match exactly the heading line (and not a heading-like
# substring inside one of the text fields above).
refs_anchor = output.find("\nReferenzen:")
assert refs_anchor != -1, f"References heading missing from serialization: {output!r}"
refs_block = output[refs_anchor + 1 :]
if not profile.references:
# No references - the block is rendered as ``Referenzen: (keine)``
# and must not contain any partner token.
assert "Partner: " not in refs_block
return
# Split into per-reference bullet lines (skip the heading line itself).
bullet_lines = [line[2:] for line in refs_block.split("\n")[1:] if line.startswith("- ")]
assert len(bullet_lines) == len(profile.references), (
f"Expected {len(profile.references)} reference bullet lines, got {len(bullet_lines)} in: {output!r}"
)
for line, ref in zip(bullet_lines, profile.references):
if ref.partner_name:
assert ref.partner_name in line, f"Partner name {ref.partner_name!r} missing from line {line!r}"
assert ref.projects.strip() in line, f"Projects text {ref.projects!r} missing from line {line!r}"
else:
assert "Partner: " not in line, (
f"Empty-partner reference line must not contain 'Partner: ' token: {line!r}"
)
@settings(max_examples=100, deadline=None)
@given(profile=_team_profile())
def test_serialize_team_profile_preserves_list_order(
profile: TeamProfile,
) -> None:
"""Property 6.6: List entry order matches the input order.
The order of competence and reference entries in the serialized
output matches the order in ``profile.competences`` and
``profile.references`` exactly (requirement 13.4).
"""
output = serialize_team_profile(profile)
# Verify competence ordering by scanning the competence block only;
# text fields above (e.g. ``team_name``) might coincidentally contain
# a ``- <name>`` substring, so we restrict the search window.
if profile.competences:
comp_start = output.find("\nKompetenzen:")
# Defensive: ``Kompetenzen:`` is always preceded by other lines
# (Teamname: etc.), so ``\nKompetenzen:`` must be present.
assert comp_start != -1
# Competence block ends just before the ``Referenzen:`` heading.
refs_start = output.find("\nReferenzen:", comp_start)
comp_block = output[comp_start:refs_start] if refs_start != -1 else output[comp_start:]
last_idx = -1
for competence in profile.competences:
# Use the bullet-prefixed form to avoid spurious matches in
# other fields (e.g. team_name).
needle = f"- {competence.name}"
idx = comp_block.find(needle, last_idx + 1)
assert idx > last_idx, (
f"Competence {competence.name!r} not found in expected order in serialization: {output!r}"
)
last_idx = idx
# Verify reference ordering by locating each ``projects`` text in
# turn within the references block (it is unique-enough because
# every reference's projects value is non-whitespace).
if profile.references:
refs_index = output.find("\nReferenzen:")
assert refs_index != -1
refs_block = output[refs_index:]
last_idx = -1
for reference in profile.references:
needle = reference.projects.strip()
idx = refs_block.find(needle, last_idx + 1)
assert idx > last_idx, (
f"Reference projects {reference.projects!r} not found in "
f"expected order in serialization: {output!r}"
)
last_idx = idx