diff --git a/app/core/chunking/chunking_strategies.py b/app/core/chunking/chunking_strategies.py index 59a8d41..af32a8c 100644 --- a/app/core/chunking/chunking_strategies.py +++ b/app/core/chunking/chunking_strategies.py @@ -191,6 +191,36 @@ def strategy_by_heading(blocks: List[RawBlock], config: Dict[str, Any], note_id: continue # FALL B: SMART MODE (Regel 1-3) + # WP-26 v1.1: Prüfe auf Section-Type-Wechsel AUCH in Schritt 2 + # Wenn sich der section_type zwischen current_meta und item ändert, muss gesplittet werden + item_section_type = item.get("section_type") + current_section_type_meta = current_meta.get("section_type") + + # Section-Type-Wechsel: Von None zu einem Typ ODER von einem Typ zu einem anderen + is_section_type_change_step2 = ( + current_chunk_text and # Es gibt bereits Content + ( + # Wechsel von None zu einem Typ + (current_section_type_meta is None and item_section_type is not None) or + # Wechsel von einem Typ zu None + (current_section_type_meta is not None and item_section_type is None) or + # Wechsel zwischen verschiedenen Typen + (current_section_type_meta is not None and item_section_type is not None + and current_section_type_meta != item_section_type) + ) + ) + + if is_section_type_change_step2: + # WP-26 v1.1: Section-Type-Wechsel erzwingt Split + _emit(current_chunk_text, current_meta["title"], current_meta["path"], + current_meta["section_type"], current_meta["block_id"]) + current_chunk_text = "" + # Reset Meta für nächsten Chunk + current_meta["title"] = item["meta"].section_title + current_meta["path"] = item["meta"].section_path + current_meta["section_type"] = item_section_type + current_meta["block_id"] = item.get("block_id") + combined_text = (current_chunk_text + "\n\n" + item_text).strip() if current_chunk_text else item_text combined_est = estimate_tokens(combined_text) diff --git a/app/core/graph/graph_utils.py b/app/core/graph/graph_utils.py index 3181816..54807f1 100644 --- a/app/core/graph/graph_utils.py +++ b/app/core/graph/graph_utils.py @@ -131,6 +131,12 @@ def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[ Trennt einen Obsidian-Link [[Target#Section]] in seine Bestandteile Target und Section. Behandelt Self-Links (z.B. [[#Ziele]]), indem die aktuelle note_id eingesetzt wird. + WP-26 v1.1: Extrahiert Block-ID aus Section-Strings. + - Wenn Section "^block-id" enthält, wird nur der Block-ID-Teil extrahiert + - Beispiel: "📖 Diagnose: Glioblastom ^kontext" -> section = "kontext" + - Beispiel: "^learning" -> section = "learning" + - Beispiel: " ^sit" (nur Block-ID) -> section = "sit" + Returns: Tuple (target_id, target_section) """ @@ -141,6 +147,16 @@ def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[ target = parts[0].strip() section = parts[1].strip() if len(parts) > 1 else None + # WP-26 v1.1: Block-ID-Extraktion aus Section + # Wenn die Section ein "^" enthält, extrahiere nur den Block-ID-Teil + if section and "^" in section: + # Finde den ^block-id Teil + import re + block_id_match = re.search(r'\^([a-zA-Z0-9_-]+)', section) + if block_id_match: + # Ersetze die gesamte Section durch nur die Block-ID + section = block_id_match.group(1) + # Spezialfall: Self-Link innerhalb derselben Datei if not target and section and current_note_id: target = current_note_id diff --git a/tests/test_wp26_section_types.py b/tests/test_wp26_section_types.py index 8d5dc8d..015df20 100644 --- a/tests/test_wp26_section_types.py +++ b/tests/test_wp26_section_types.py @@ -477,6 +477,81 @@ Meine positive Einstellung hat mir geholfen. # Der insight-Chunk sollte den Lektions-Inhalt enthalten insight_text = insight_chunks[0].text assert "durchdenken" in insight_text.lower() or "positive" in insight_text.lower() + + def test_section_type_change_in_smart_mode_forces_split(self): + """WP-26 v1.1 Fix: Section-Type-Wechsel erzwingt Split auch in SMART MODE (Schritt 2)""" + md = """ +## Section A ohne Typ + +Inhalt A ohne section_type. + +## Section B ohne Typ + +Inhalt B ohne section_type. + +## Section C mit Typ +> [!section] insight + +Inhalt C mit section_type "insight". +""" + blocks, _ = parse_blocks(md) + + # SMART MODE: strict=False, smart_edge=True + # Token-Limit hoch genug, dass alles zusammengefasst werden KÖNNTE + config = { + "target": 2000, + "max": 4000, + "split_level": 2, + "strict_heading_split": False, + "enable_smart_edge_allocation": True + } + + chunks = strategy_by_heading(blocks, config, "test-note") + + # Trotz hohem Token-Limit sollte Section C ein separater Chunk sein + # wegen Section-Type-Wechsel (None -> insight) + assert len(chunks) >= 2, f"Erwartet mindestens 2 Chunks, bekommen: {len(chunks)}" + + # Der letzte Chunk sollte section_type "insight" haben + insight_chunks = [c for c in chunks if c.section_type == "insight"] + assert len(insight_chunks) >= 1, "Kein Chunk mit section_type 'insight' gefunden" + + +class TestBlockIdParsing: + """UT-18: Block-ID-Extraktion aus Section-Referenzen""" + + def test_block_id_extraction_from_section(self): + """Block-ID wird aus Section-String extrahiert""" + from app.core.graph.graph_utils import parse_link_target + + # Test: Überschrift mit Block-ID + target, section = parse_link_target("#📖 Diagnose: Glioblastom ^kontext", "note1") + assert target == "note1" # Self-Link + assert section == "kontext", f"Erwartet 'kontext', bekommen: {section}" + + def test_block_id_extraction_only_caret(self): + """Nur Block-ID mit ^""" + from app.core.graph.graph_utils import parse_link_target + + target, section = parse_link_target("#^learning", "note1") + assert target == "note1" + assert section == "learning" + + def test_block_id_extraction_with_spaces(self): + """Block-ID mit Text davor""" + from app.core.graph.graph_utils import parse_link_target + + target, section = parse_link_target("OtherNote#🎭 Emotions-Check ^emotionen", None) + assert target == "OtherNote" + assert section == "emotionen" + + def test_section_without_block_id(self): + """Section ohne Block-ID bleibt unverändert""" + from app.core.graph.graph_utils import parse_link_target + + target, section = parse_link_target("Note#Normale Überschrift", None) + assert target == "Note" + assert section == "Normale Überschrift" if __name__ == "__main__":