Skip to content

feat(orchestrator): Kandidaten-Dedup + balancierter Cap fuer Hybrid-Buchplanung (#346, PR 2/3)#347

Merged
TillQuandel merged 1 commit into
masterfrom
feat/346-candidate-dedup
Jul 18, 2026
Merged

feat(orchestrator): Kandidaten-Dedup + balancierter Cap fuer Hybrid-Buchplanung (#346, PR 2/3)#347
TillQuandel merged 1 commit into
masterfrom
feat/346-candidate-dedup

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

PR 2/3 zu #346 — isolierte Hybrid-Buchplanungs-Bausteine

Teil des Implementierungsplans zu #346 (Hybrid-Buchplanung: Planner je Hauptkapitel, Extraktion global). Dieser PR liefert zwei reine, vollständig getestete Funktionen in generative/orchestrator.py (Nachbarschaft von dedup_exact) — ohne Orchestrator-Wiring. Die Verdrahtung (--book-mode, Budget-Formel, Per-Kapitel-Planner) folgt in PR 3. Bewusst noch tote, aber verifizierte Funktionen: isolierte Review-Fläche.

Ref #346 (schließt das Issue NICHT — das erledigt erst PR 3).

Funktion 1: dedup_concept_candidates(concepts) -> tuple[list, int]

Globale Kandidaten-Dedup über den normalisierten Titel (dieselbe _normalize()-Logik wie dedup_exact), damit dasselbe Konzept aus mehreren Kapiteln EIN Konzept wird.

  • origin == "secondary_mention" ist ein eigener Kanal: passiert unangetastet, wird nie dedupliziert/verworfen und kollidiert nicht mit primären Titeln.
  • Bei Titel-Duplikat gewinnt die stärkere Fassung: höhere priority (high=2, medium=1, low=0) schlägt niedrigere; bei Gleichstand schlägt eine non-skip-Fassung einen skip-Survivor; sonst Erstauftreten. Der Survivor behält die Erstauftreten-Position (stabile Reihenfolge).
  • Je verworfenem Duplikat ein Funnel-Event: _trace_stage_outcome(titel, "planner_dedup", "dropped", drop_reason="chapter_duplicate", detail="survivor=<titel>").
  • Rückgabe: (deduplizierte Liste, Anzahl verworfener Duplikate).

Funktion 2: cap_candidates_balanced(concepts, budget, chapter_word_counts, chapter_keys) -> tuple[list, list]

Wortanteil-balancierter, prioritätssortierter Kandidaten-Cap.

  • budget ist fertig berechnet (Budget-Formel lebt beim Aufrufer, PR 3).
  • Erst ALLE high-priority-Kandidaten über alle Kapitel. Überlauf-Regel (highs > Budget): Kürzung proportional zum Wortanteil des Herkunfts-Kapitels.
  • Rest-Slots nach Kapitel-Wortanteil verteilt (großes Kapitel → mehr Slots), innerhalb eines Kapitels nach Priorität, dann Erstauftreten.
  • secondary_mention zählt nicht gegen das Budget und wird nie gekappt.
  • Rückgabe: (behaltene, gekappte), beide in Original-Eingabereihenfolge.

Die Slot-Verteilung nutzt das D'Hondt-Höchstzahlverfahren (weight/(alloc+1)) — deterministisch, Tie-Break Richtung frühere Kapitel-Reihenfolge.

Design-Entscheidungen

Kapitel-Key: paralleler Parameter statt ConceptItem.chapter. Der Auftrag verlangte zu sichten, ob das vorhandene chapter-Feld als Kapitel-Key taugt. Entscheidung: nein, stattdessen chapter_keys: list[str] (index-aligned zu concepts). Begründung: ConceptItem.chapter ist laut Schema-Kommentar LLM-Freitext ("Kapitel/Abschnitt wo das Konzept erwartet wird") in möglicher Unterabschnitts-Granularität — nicht garantiert deckungsgleich mit den deterministischen Outline-Kapitel-Keys, aus denen chapter_word_counts in PR 1 gebildet wird. Eine Kopplung an dieses Feld würde die Balancierung von unzuverlässigem LLM-Text abhängig machen und mit chapter_word_counts fehlausrichten. PR 3 kennt beim Per-Kapitel-Planner-Call den autoritativen Key ohnehin und reicht ihn durch. Vorteil: Funktion bleibt rein/deterministisch, keine Schema-Änderung nötig (Sperrzone respektiert).

Abgrenzung zu cap_actionable_concepts (runtime_config.py:273). Das bestehende Muster (uniformer Cap ohne Wortanteil-Balancierung) bleibt unangetastet — es bedient weiter Normal-/by-chapter-Pfad. cap_candidates_balanced ist ein Sibling für den book-mode-Pfad; gleiche (kept, capped)-Rückgabesemantik. Budget-neutral ist hier — dem Plan folgend — nur secondary_mention; skip-action-Kandidaten (falls vorhanden) werden wie normale Kandidaten budgetiert. PR 3 kann Skips vor dem Cap filtern, wie es der Normalpfad tut (orchestrator.py, actionable-Filter).

TDD / RED-Nachweis

Neue Testdatei generative/tests/test_candidate_dedup_and_cap.py (14 Tests, Capture-Muster wie test_stage_outcome_events.py). Erst RED (Funktionen fehlten):

14 failed in 2.89s
E   AttributeError: module 'generative.orchestrator' has no attribute 'dedup_concept_candidates'
E   AttributeError: module 'generative.orchestrator' has no attribute 'cap_candidates_balanced'

Nach Implementierung GREEN:

14 passed in 1.71s

Abgedeckte Fälle — Dedup: Einzelvorkommen; normalisiertes Duplikat über Kapitel; Priority-Upgrade; skip→actionable-Upgrade (+ Umkehrung); secondary_mention unangetastet (inkl. Kollision mit primary); Funnel-Event mit stage/drop_reason/detail-Assertions; leere Eingabe. Cap: highs>Budget-Überlauf proportional; Wortanteil-Verteilung groß vs. klein; Priorität innerhalb Kapitel; secondary_mention budget-neutral; Budget > Kandidatenzahl = no-op; deterministische Stabilität bei Gleichstand.

Verifikation

  • uv sync --extra dev --locked — ok, uv.lock unverändert.
  • Import-Herkunft verifiziert: generative.orchestratorC:\tmp\wt-346a\generative\orchestrator.py (Worktree).
  • Volle Suite uv run pytest generative lib/decision_engine/tests shared/tests -q: 6267 passed, 3 skipped, 9 deselected, 45 warnings in 669.00s (exit 0).
  • ruff check + ruff format --check auf beiden Dateien: grün.

Geänderte Dateien

  • generative/orchestrator.py (+176; nur neue Funktionen, kein Aufruf aus main()/Stages)
  • generative/tests/test_candidate_dedup_and_cap.py (neu)

Kein Anfassen von pdf_chunker.py (paralleler PR-1-Builder) und keine Schema-Änderung.

@TillQuandel
TillQuandel merged commit 9c9a796 into master Jul 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants