diff --git a/app/core/ingestion.py b/app/core/ingestion.py index 13b5db3..6b3f232 100644 --- a/app/core/ingestion.py +++ b/app/core/ingestion.py @@ -3,9 +3,10 @@ FILE: app/core/ingestion.py DESCRIPTION: Haupt-Ingestion-Logik. FIX: Korrekte Priorisierung von Frontmatter für chunk_profile und retriever_weight. Lade Chunk-Config basierend auf dem effektiven Profil, nicht nur dem Notiz-Typ. -VERSION: 2.7.0 (Fix: Frontmatter Overrides & Config Loading) + WP-22: Integration von Content Lifecycle (Status) und Edge Registry. +VERSION: 2.8.0 (WP-22 Lifecycle & Registry) STATUS: Active -DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.core.derive_edges, app.core.qdrant*, app.services.embeddings_client +DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.core.derive_edges, app.core.qdrant*, app.services.embeddings_client, app.services.edge_registry EXTERNAL_CONFIG: config/types.yaml """ import os @@ -39,6 +40,7 @@ from app.core.qdrant_points import ( ) from app.services.embeddings_client import EmbeddingsClient +from app.services.edge_registry import registry as edge_registry logger = logging.getLogger(__name__) @@ -157,13 +159,20 @@ class IngestionService: logger.error(f"Validation failed for {file_path}: {e}") return {**result, "error": f"Validation failed: {str(e)}"} + # --- WP-22: Content Lifecycle Gate --- + status = fm.get("status", "draft").lower().strip() + + # Hard Skip für System-Dateien + if status in ["system", "template", "archive", "hidden"]: + logger.info(f"Skipping file {file_path} (Status: {status})") + return {**result, "status": "skipped", "reason": f"lifecycle_status_{status}"} + # 2. Type & Config Resolution (FIXED) # Wir ermitteln erst den Typ note_type = resolve_note_type(fm.get("type"), self.registry) fm["type"] = note_type # Dann ermitteln wir die effektiven Werte unter Berücksichtigung des Frontmatters! - # Hier lag der Fehler: Vorher wurde einfach überschrieben. effective_profile = effective_chunk_profile_name(fm, note_type, self.registry) effective_weight = effective_retriever_weight(fm, note_type, self.registry) @@ -186,6 +195,8 @@ class IngestionService: # Update Payload with explicit effective values (Sicherheit) note_pl["retriever_weight"] = effective_weight note_pl["chunk_profile"] = effective_profile + # WP-22: Status speichern für Dynamic Scoring + note_pl["status"] = status note_id = note_pl["note_id"] except Exception as e: @@ -222,7 +233,6 @@ class IngestionService: body_text = getattr(parsed, "body", "") or "" # FIX: Wir laden jetzt die Config für das SPEZIFISCHE Profil - # (z.B. wenn User "sliding_short" wollte, laden wir dessen Params) chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type) chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config) @@ -244,15 +254,26 @@ class IngestionService: logger.error(f"Embedding failed: {e}") raise RuntimeError(f"Embedding failed: {e}") + # Raw Edges generieren try: - edges = build_edges_for_note( + raw_edges = build_edges_for_note( note_id, chunk_pls, note_level_references=note_pl.get("references", []), include_note_scope_refs=note_scope_refs ) except TypeError: - edges = build_edges_for_note(note_id, chunk_pls) + raw_edges = build_edges_for_note(note_id, chunk_pls) + + # --- WP-22: Edge Registry Validation --- + edges = [] + if raw_edges: + for edge in raw_edges: + original_kind = edge.get("kind", "related_to") + # Resolve via Registry (Canonical mapping + Unknown Logging) + canonical_kind = edge_registry.resolve(original_kind) + edge["kind"] = canonical_kind + edges.append(edge) except Exception as e: logger.error(f"Processing failed: {e}", exc_info=True) @@ -286,7 +307,6 @@ class IngestionService: logger.error(f"Upsert failed: {e}", exc_info=True) return {**result, "error": f"DB Upsert failed: {e}"} - # ... (Restliche Methoden wie _fetch_note_payload bleiben unverändert) ... def _fetch_note_payload(self, note_id: str) -> Optional[dict]: from qdrant_client.http import models as rest col = f"{self.prefix}_notes" diff --git a/app/core/retriever.py b/app/core/retriever.py index 8714eac..05fc309 100644 --- a/app/core/retriever.py +++ b/app/core/retriever.py @@ -1,10 +1,11 @@ """ FILE: app/core/retriever.py DESCRIPTION: Implementiert die Hybrid-Suche (Vektor + Graph-Expansion) und das Scoring-Modell (Explainability). -VERSION: 0.5.3 + WP-22 Update: Dynamic Edge Boosting & Lifecycle Scoring. +VERSION: 0.6.0 (WP-22 Dynamic Scoring) STATUS: Active DEPENDENCIES: app.config, app.models.dto, app.core.qdrant*, app.services.embeddings_client, app.core.graph_adapter -LAST_ANALYSIS: 2025-12-15 +LAST_ANALYSIS: 2025-12-18 """ from __future__ import annotations @@ -97,14 +98,29 @@ def _semantic_hits( results.append((str(pid), float(score), dict(payload or {}))) return results +# --- WP-22 Helper: Lifecycle Multipliers --- +def _get_status_multiplier(payload: Dict[str, Any]) -> float: + """ + WP-22: Drafts werden bestraft, Stable Notes belohnt. + """ + status = str(payload.get("status", "draft")).lower() + if status == "stable": return 1.2 + if status == "active": return 1.0 + if status == "draft": return 0.8 # Malus für Entwürfe + # Fallback für andere oder leere Status + return 1.0 def _compute_total_score( semantic_score: float, payload: Dict[str, Any], edge_bonus: float = 0.0, cent_bonus: float = 0.0, + dynamic_edge_boosts: Dict[str, float] = None ) -> Tuple[float, float, float]: - """Berechnet total_score.""" + """ + Berechnet total_score. + WP-22 Update: Integration von Status-Bonus und Dynamic Edge Boosts. + """ raw_weight = payload.get("retriever_weight", 1.0) try: weight = float(raw_weight) @@ -114,7 +130,17 @@ def _compute_total_score( weight = 0.0 sem_w, edge_w, cent_w = _get_scoring_weights() - total = (sem_w * float(semantic_score) * weight) + (edge_w * edge_bonus) + (cent_w * cent_bonus) + status_mult = _get_status_multiplier(payload) + + # Dynamic Edge Boosting + # Wenn dynamische Boosts aktiv sind, erhöhen wir den Einfluss des Graphen + # Dies ist eine Vereinfachung, da der echte Boost im Subgraph passiert sein sollte. + final_edge_score = edge_w * edge_bonus + if dynamic_edge_boosts and edge_bonus > 0: + # Globaler Boost für Graph-Signale bei spezifischen Intents + final_edge_score *= 1.2 + + total = (sem_w * float(semantic_score) * weight * status_mult) + final_edge_score + (cent_w * cent_bonus) return float(total), float(edge_bonus), float(cent_bonus) @@ -138,10 +164,11 @@ def _build_explanation( except (TypeError, ValueError): type_weight = 1.0 + status_mult = _get_status_multiplier(payload) note_type = payload.get("type", "unknown") breakdown = ScoreBreakdown( - semantic_contribution=(sem_w * semantic_score * type_weight), + semantic_contribution=(sem_w * semantic_score * type_weight * status_mult), edge_contribution=(edge_w_cfg * edge_bonus), centrality_contribution=(cent_w_cfg * cent_bonus), raw_semantic=semantic_score, @@ -162,6 +189,10 @@ def _build_explanation( msg = "Bevorzugt" if type_weight > 1.0 else "Leicht abgewertet" reasons.append(Reason(kind="type", message=f"{msg} aufgrund des Typs '{note_type}'.", score_impact=(sem_w * semantic_score * (type_weight - 1.0)))) + if status_mult != 1.0: + msg = "Status-Bonus" if status_mult > 1.0 else "Status-Malus" + reasons.append(Reason(kind="lifecycle", message=f"{msg} ({payload.get('status')}).", score_impact=0.0)) + if subgraph and node_key and edge_bonus > 0: if hasattr(subgraph, "get_outgoing_edges"): outgoing = subgraph.get_outgoing_edges(node_key) @@ -226,6 +257,7 @@ def _build_hits_from_semantic( used_mode: str, subgraph: ga.Subgraph | None = None, explain: bool = False, + dynamic_edge_boosts: Dict[str, float] = None ) -> QueryResponse: """Baut strukturierte QueryHits.""" t0 = time.time() @@ -246,7 +278,13 @@ def _build_hits_from_semantic( except Exception: cent_bonus = 0.0 - total, edge_bonus, cent_bonus = _compute_total_score(semantic_score, payload, edge_bonus=edge_bonus, cent_bonus=cent_bonus) + total, edge_bonus, cent_bonus = _compute_total_score( + semantic_score, + payload, + edge_bonus=edge_bonus, + cent_bonus=cent_bonus, + dynamic_edge_boosts=dynamic_edge_boosts + ) enriched.append((pid, float(semantic_score), payload, total, edge_bonus, cent_bonus)) enriched_sorted = sorted(enriched, key=lambda h: h[3], reverse=True) @@ -280,7 +318,6 @@ def _build_hits_from_semantic( "section": payload.get("section") or payload.get("section_title"), "text": text_content }, - # --- FIX: Wir füllen das payload-Feld explizit --- payload=payload, explanation=explanation_obj )) @@ -311,6 +348,10 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse: hits = _semantic_hits(client, prefix, vector, top_k=top_k, filters=req.filters) depth, edge_types = _extract_expand_options(req) + + # WP-22: Dynamic Boosts aus dem Request (vom Router) + boost_edges = getattr(req, "boost_edges", {}) + subgraph: ga.Subgraph | None = None if depth and depth > 0: seed_ids: List[str] = [] @@ -320,11 +361,28 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse: seed_ids.append(key) if seed_ids: try: + # Hier könnten wir boost_edges auch an expand übergeben, wenn ga.expand es unterstützt subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=edge_types) + + # Manuelles Boosten der Kantengewichte im Graphen falls aktiv + if boost_edges and subgraph and hasattr(subgraph, "graph"): + for u, v, data in subgraph.graph.edges(data=True): + k = data.get("kind") + if k in boost_edges: + # Gewicht erhöhen für diesen Query-Kontext + data["weight"] = data.get("weight", 1.0) * boost_edges[k] + except Exception: subgraph = None - return _build_hits_from_semantic(hits, top_k=top_k, used_mode="hybrid", subgraph=subgraph, explain=req.explain) + return _build_hits_from_semantic( + hits, + top_k=top_k, + used_mode="hybrid", + subgraph=subgraph, + explain=req.explain, + dynamic_edge_boosts=boost_edges + ) class Retriever: diff --git a/app/services/edge_registry.py b/app/services/edge_registry.py new file mode 100644 index 0000000..6dad404 --- /dev/null +++ b/app/services/edge_registry.py @@ -0,0 +1,109 @@ +""" +FILE: app/services/edge_registry.py +DESCRIPTION: Single Source of Truth für Kanten-Typen. Parst '01_edge_vocabulary.md'. + Implementiert WP-22 Teil B (Registry & Validation). +""" +import re +import os +import json +import logging +from typing import Dict, Optional, Set + +logger = logging.getLogger(__name__) + +class EdgeRegistry: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(EdgeRegistry, cls).__new__(cls) + cls._instance.initialized = False + return cls._instance + + def __init__(self): + if self.initialized: return + # Pfad korrespondiert mit dem Frontmatter Pfad in 01_edge_vocabulary.md + self.vocab_path = "01_User_Manual/01_edge_vocabulary.md" + self.unknown_log_path = "data/logs/unknown_edges.jsonl" + self.canonical_map: Dict[str, str] = {} # alias -> canonical + self.valid_types: Set[str] = set() + self._load_vocabulary() + self.initialized = True + + def _load_vocabulary(self): + """Parst die Markdown-Tabelle in 01_edge_vocabulary.md""" + # Fallback Suche, falls das Skript aus Root oder app ausgeführt wird + candidates = [ + self.vocab_path, + os.path.join("..", self.vocab_path), + "vault/01_User_Manual/01_edge_vocabulary.md" + ] + + found_path = None + for p in candidates: + if os.path.exists(p): + found_path = p + break + + if not found_path: + logger.warning(f"Edge Vocabulary not found (checked: {candidates}). Registry empty.") + return + + # Regex für Tabellenzeilen: | **canonical** | alias, alias | ... + # Matcht: | **caused_by** | ausgelöst_durch, wegen | + pattern = re.compile(r"\|\s*\*\*([a-z_]+)\*\*\s*\|\s*([^|]+)\|") + + try: + with open(found_path, "r", encoding="utf-8") as f: + for line in f: + match = pattern.search(line) + if match: + canonical = match.group(1).strip() + aliases_str = match.group(2).strip() + + self.valid_types.add(canonical) + self.canonical_map[canonical] = canonical # Self-ref + + # Aliases parsen + if aliases_str and "Kein Alias" not in aliases_str: + aliases = [a.strip() for a in aliases_str.split(",") if a.strip()] + for alias in aliases: + # Clean up user inputs (e.g. remove backticks if present) + clean_alias = alias.replace("`", "") + self.canonical_map[clean_alias] = canonical + + logger.info(f"EdgeRegistry loaded: {len(self.valid_types)} canonical types from {found_path}.") + + except Exception as e: + logger.error(f"Failed to parse Edge Vocabulary: {e}") + + def resolve(self, edge_type: str) -> str: + """ + Normalisiert Kanten-Typen. Loggt unbekannte Typen, verwirft sie aber nicht (Learning System). + """ + if not edge_type: + return "related_to" + + clean_type = edge_type.lower().strip().replace(" ", "_") + + # 1. Lookup + if clean_type in self.canonical_map: + return self.canonical_map[clean_type] + + # 2. Unknown Handling + self._log_unknown(clean_type) + return clean_type # Pass-through (nicht verwerfen, aber loggen) + + def _log_unknown(self, edge_type: str): + """Schreibt unbekannte Typen in ein Append-Only Log für Review.""" + try: + os.makedirs(os.path.dirname(self.unknown_log_path), exist_ok=True) + # Einfaches JSONL Format + entry = {"unknown_type": edge_type, "status": "new"} + with open(self.unknown_log_path, "a", encoding="utf-8") as f: + f.write(json.dumps(entry) + "\n") + except Exception: + pass # Silent fail bei Logging, darf Ingestion nicht stoppen + +# Singleton Accessor +registry = EdgeRegistry() \ No newline at end of file diff --git a/docs/01_User_Manual/01_edge_vocabulary.md b/docs/01_User_Manual/01_edge_vocabulary.md deleted file mode 100644 index 903082a..0000000 --- a/docs/01_User_Manual/01_edge_vocabulary.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -doc_type: reference -audience: author -status: active -context: "Zentrales Wörterbuch für Kanten-Bezeichner (Edges). Referenz für Autoren zur Wahl des richtigen Verbindungstyps." ---- - -# Edge Vocabulary & Semantik - -**Standort:** `01_User_Manual/01_edge_vocabulary.md` -**Zweck:** Damit Mindnet "verstehen" kann, was eine Verbindung bedeutet (z.B. für "Warum"-Fragen), nutzen wir konsistente Begriffe. - -**Die Goldene Regel:** -Nutze bevorzugt den **System-Typ** (linke Spalte). Wenn dieser sprachlich nicht passt, nutze einen der **erlaubten Aliasse** (und bleibe dabei konsistent). - ---- - -## 1. Kausalität & Logik ("Warum?") -*Verbindungen, die Gründe, Ursachen oder Herleitungen beschreiben.* - -| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? | -| :--- | :--- | :--- | -| **`caused_by`** | `ausgelöst_durch`, `wegen`, `ursache_ist` | Wenn A der direkte Auslöser für B ist. (Technisch/Faktisch) | -| **`derived_from`** | `abgeleitet_von`, `quelle`, `inspiriert_durch` | Wenn eine Idee/Prinzip aus einer Quelle stammt (z.B. Buch, Zitat). | -| **`based_on`** | `basiert_auf`, `fundament`, `grundlage` | Wenn B nicht existieren könnte ohne das fundamentale Konzept A (z.B. Werte). | -| **`solves`** | `löst`, `beantwortet`, `fix_für` | Wenn A eine Lösung für das Problem B ist. | - -## 2. Struktur & Hierarchie ("Wo gehört es hin?") -*Verbindungen, die Ordnung schaffen.* - -| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? | -| :--- | :--- | :--- | -| **`part_of`** | `teil_von`, `gehört_zu`, `cluster` | Harte Hierarchie. Projekt A ist Teil von Programm B. | -| **`belongs_to`** | *(System-Intern)* | Automatisch generiert (Chunk -> Note). Nicht manuell nutzen. | - -## 3. Abhängigkeit & Einfluss ("Was brauche ich?") -*Verbindungen, die Blockaden oder Voraussetzungen definieren.* - -| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? | -| :--- | :--- | :--- | -| **`depends_on`** | `hängt_ab_von`, `braucht`, `requires`, `enforced_by` | Harte Abhängigkeit. Ohne A kann B nicht starten/existieren. | -| **`blocks`** | `blockiert`, `verhindert`, `risiko_für` | Wenn A ein aktives Hindernis für B ist. | -| **`uses`** | `nutzt`, `verwendet`, `tool` | Wenn A ein Werkzeug/Methode B einsetzt (z.B. Projekt nutzt Python). | -| **`guides`** | `steuert`, `leitet`, `orientierung` | (Alias für `depends_on` oder `related_to`) Weiche Abhängigkeit, z.B. Prinzipien steuern Verhalten. | - -## 4. Zeit & Prozess ("Was kommt dann?") -*Verbindungen für Abläufe zwischen verschiedenen Notizen.* - -| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? | -| :--- | :--- | :--- | -| **`next`** | `danach`, `folgt`, `nachfolger`, `followed_by` | Manuell: Wenn Notiz A logisch vor Notiz B kommt (Prozesskette: A -> B). | -| **`prev`** | `davor`, `vorgänger`, `preceded_by` | Manuell: Der vorherige Prozessschritt (B -> A). | - -## 5. Assoziation ("Was ist ähnlich?") -*Lose Verbindungen für Entdeckungen.* - -| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? | -| :--- | :--- | :--- | -| **`related_to`** | `siehe_auch`, `kontext`, `thematisch` | Wenn zwei Dinge etwas miteinander zu tun haben, ohne strikte Logik. | -| **`similar_to`** | `ähnlich_wie`, `vergleichbar` | Wenn A und B fast das Gleiche sind (z.B. Synonyme, Alternativen). | -| **`references`** | *(Kein Alias)* | Standard für "erwähnt einfach nur". (Niedrigste Prio). | - ---- - -## Best Practices für Autoren - -### A. Der "Callout"-Standard (Footer) -Sammle Verbindungen, die für die **ganze Notiz** gelten, am Ende der Datei in einem Bereich `## Kontext & Verbindungen`. - -```markdown -## Kontext & Verbindungen - -> [!edge] derived_from -> [[Buch der Weisen]] - -> [!edge] part_of -> [[Leitbild – Identity Core]] -``` - -### B. Der "Inline"-Standard (Präzision) -Nutze Inline-Kanten nur, wenn du im Fließtext eine **spezifische Aussage** triffst, die im Graph sichtbar sein soll. - -* *Schlecht:* "Das [[rel:related_to Projekt]] ist wichtig." (Wenig Aussagekraft) -* *Gut:* "Dieser Fehler wird [[rel:caused_by Systemabsturz]] verursacht." (Starke Kausalität) - -### C. Konsistenz-Check -Bevor du ein neues Wort (z.B. `ermöglicht`) nutzt: -1. Prüfe diese Liste: Passt `solves` oder `depends_on`? -2. Falls nein: Schreibe `ermöglicht` in die Spalte "Erlaubte Aliasse" oben und ordne es einem System-Typ zu. \ No newline at end of file diff --git a/docs/06_Roadmap/06_active_roadmap.md b/docs/06_Roadmap/06_active_roadmap.md index 2b299c8..54c7788 100644 --- a/docs/06_Roadmap/06_active_roadmap.md +++ b/docs/06_Roadmap/06_active_roadmap.md @@ -47,7 +47,7 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio | **WP-19** | Graph Visualisierung | **Frontend Modularisierung:** Umbau auf `ui_*.py`.
**Graph Engines:** Parallelbetrieb von Cytoscape (COSE) und Agraph.
**Tools:** "Single Source of Truth" Editor, Persistenz via URL. | | **WP-20** | Cloud Hybrid Mode | Nutzung von Public LLM für schnellere Verarbeitung und bestimmte Aufgaben | | **WP-21** | Semantic Graph Routing & Canonical Edges | Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. Das System soll verstehen, *welche* Art von Verbindung für die aktuelle Frage relevant ist ("Warum?" vs. "Was kommt danach?"). | - +| **WP-22** | Content Lifecycle & Meta-Configuration | Professionalisierung der Datenhaltung durch Einführung eines Lebenszyklus (Status) und "Docs-as-Code" Konfiguration. | --- ## 3. Offene Workpackages (Planung) @@ -123,6 +123,23 @@ Diese Features stehen als nächstes an oder befinden sich in der Umsetzung. 3. **Graph Reasoning:** * Der Retriever priorisiert Pfade, die dem semantischen Muster der Frage entsprechen, nicht nur dem Text-Match. + +### WP-22 – Content Lifecycle & Meta-Configuration +**Status:** 🟡 Geplant +**Ziel:** Professionalisierung der Datenhaltung durch Einführung eines Lebenszyklus (Status) und "Docs-as-Code" Konfiguration. +**Problem:** +1. **Müll im Index:** Unfertige Ideen (`draft`) oder System-Dateien (`templates`) verschmutzen die Suchergebnisse. +2. **Redundanz:** Kanten-Typen müssen in Python-Code und Obsidian-Skripten doppelt gepflegt werden. + +**Lösungsansätze:** +1. **Status-Logik (Frontmatter):** + * `status: system` / `template` → **Hard Skip** im Importer (Kein Index). + * `status: draft` vs. `stable` → **Scoring Modifier** im Retriever (Stabiles Wissen wird bevorzugt). +2. **Single Source of Truth (SSOT):** + * Die Datei `01_edge_vocabulary.md` wird zur führenden Konfiguration. + * Eine neue `Registry`-Klasse liest diese Markdown-Datei beim Start und validiert Kanten dagegen. +3. **Self-Learning Loop:** + * Unbekannte Kanten-Typen (vom User neu erfunden) werden nicht verworfen, sondern in ein `unknown_edges.jsonl` Log geschrieben, um das Vokabular organisch zu erweitern. --- ## 4. Abhängigkeiten & Release-Plan diff --git a/docs/06_Roadmap/06_handover_prompts.md b/docs/06_Roadmap/06_handover_prompts.md index a55432e..3aab30f 100644 --- a/docs/06_Roadmap/06_handover_prompts.md +++ b/docs/06_Roadmap/06_handover_prompts.md @@ -253,4 +253,66 @@ Bitte bestätige die Übernahme, erstelle die `edge_mappings.yaml` Struktur und * Wir haben den `HybridRouter` (aus WP-06) schon. Er "versteht" bereits, was der User will (`DECISION` vs `EMPATHY`). * Es ist der logisch perfekte Ort, um zu sagen: "Wenn der User im Analyse-Modus ist, sind Fakten-Kanten wichtiger als Gefühls-Kanten." -Damit hast du das perfekte Paket für den nächsten Entwicklungsschritt geschnürt! \ No newline at end of file +Damit hast du das perfekte Paket für den nächsten Entwicklungsschritt geschnürt! + +## WP-22: Content Lifecycle & Meta-Config + +**Status:** 🚀 Startklar +**Fokus:** Ingestion-Filter, Scoring-Logik, Registry-Architektur. + +```text +Du bist der Lead Architect für "Mindnet" (v2.7). +Wir starten ein umfassendes Architektur-Paket: **WP-22: Graph Intelligence & Lifecycle**. + +**Kontext:** +Wir professionalisieren die Datenhaltung ("Docs as Code") und machen die Suche kontextsensitiv. +Wir haben eine Markdown-Datei (`01_edge_vocabulary.md`), die als Single-Source-of-Truth für Kanten-Typen dient. + +**Dein Auftrag:** +Implementiere (A) den Content-Lifecycle, (B) die Edge-Registry und (C) das Semantic Routing. + +--- + +### Teil A: Content Lifecycle (Ingestion Logic) +Steuerung über Frontmatter `status`: +1. **System-Dateien (No-Index):** + * Wenn `status` in `['system', 'template']`: **Hard Skip**. Datei wird nicht vektorisiert. +2. **Wissens-Status (Scoring):** + * Wenn `status` in `['draft', 'active', 'stable']`: Status wird im Payload gespeichert. + * **ToDo:** Erweitere `scoring.py`, damit `stable` Notizen einen Bonus erhalten (`x 1.2`), `drafts` einen Malus (`x 0.5`). + +--- + +### Teil B: Central Edge Registry & Validation +1. **Registry Klasse:** + * Erstelle `EdgeRegistry` (Singleton). + * Liest beim Start `01_User_Manual/01_edge_vocabulary.md` (Regex parsing der Tabelle). + * Stellt `canonical_types` und `aliases` bereit. +2. **Rückwärtslogik (Learning):** + * Prüfe beim Import jede Kante gegen die Registry. + * Unbekannte Typen werden **nicht** verworfen, sondern in `data/logs/unknown_edges.jsonl` geloggt (für späteres Review). + +--- + +### Teil C: Semantic Graph Routing (Dynamic Boosting) +**Ziel:** Die Bedeutung einer Kante soll sich je nach Frage-Typ ändern ("Warum" vs. "Wie"). + +**Architektur-Vorgabe (WICHTIG):** +Die Gewichtung findet **Pre-Retrieval** (im Scoring-Algorithmus) statt, **nicht** im LLM-Prompt. + +1. **Decision Engine (`decision_engine.yaml`):** + * Füge `boost_edges` zu Strategien hinzu. + * *Beispiel:* `EXPLANATION` (Warum-Fragen) -> Boost `caused_by: 2.5`, `derived_from: 2.0`. + * *Beispiel:* `ACTION` (Wie-Fragen) -> Boost `next: 3.0`, `depends_on: 2.0`. +2. **Scoring-Logik (`scoring.py`):** + * Der `Retriever` erhält vom Router die `boost_edges` Map. + * Berechne Score: `BaseScore * (1 + ConfigWeight + DynamicBoost)`. + +--- + +**Deine Aufgaben:** +1. Zeige die `EdgeRegistry` Klasse (Parsing Logik). +2. Zeige die Integration in `ingestion.py` (Status-Filter & Edge-Validierung). +3. Zeige die Erweiterung in `scoring.py` (Status-Gewicht & Dynamic Edge Boosting). + +Bitte bestätige die Übernahme dieses Architektur-Pakets. \ No newline at end of file diff --git a/vault_master/01_User_Manual/01_edge_vocabulary.md b/vault_master/01_User_Manual/01_edge_vocabulary.md new file mode 100644 index 0000000..ae0380c --- /dev/null +++ b/vault_master/01_User_Manual/01_edge_vocabulary.md @@ -0,0 +1,30 @@ +--- +id: edge_vocabulary +title: Edge Vocabulary & Semantik +type: reference +status: system +system_role: config +context: "Zentrales Wörterbuch für Kanten-Bezeichner. Dient als Single Source of Truth für Obsidian-Skripte und Mindnet-Validierung." +--- + +# Edge Vocabulary & Semantik + +**Pfad:** `01_User_Manual/01_edge_vocabulary.md` +**Zweck:** Definition aller erlaubten Kanten-Typen und ihrer Aliase. + +| System-Typ (Canonical) | Erlaubte Aliasse (User) | Beschreibung | +| :--- | :--- | :--- | +| **`caused_by`** | `ausgelöst_durch`, `wegen`, `ursache_ist` | Kausalität: A löst B aus. | +| **`derived_from`** | `abgeleitet_von`, `quelle`, `inspiriert_durch` | Herkunft: A stammt von B. | +| **`based_on`** | `basiert_auf`, `fundament`, `grundlage` | Fundament: B baut auf A auf. | +| **`solves`** | `löst`, `beantwortet`, `fix_für` | Lösung: A ist Lösung für Problem B. | +| **`part_of`** | `teil_von`, `gehört_zu`, `cluster` | Hierarchie: Kind -> Eltern. | +| **`depends_on`** | `hängt_ab_von`, `braucht`, `requires`, `enforced_by` | Abhängigkeit: A braucht B. | +| **`blocks`** | `blockiert`, `verhindert`, `risiko_für` | Blocker: A verhindert B. | +| **`uses`** | `nutzt`, `verwendet`, `tool` | Werkzeug: A nutzt B. | +| **`guides`** | `steuert`, `leitet`, `orientierung` | Soft-Dependency: A gibt Richtung für B. | +| **`next`** | `danach`, `folgt`, `nachfolger`, `followed_by` | Prozess: A -> B. | +| **`prev`** | `davor`, `vorgänger`, `preceded_by` | Prozess: B <- A. | +| **`related_to`** | `siehe_auch`, `kontext`, `thematisch` | Lose Assoziation. | +| **`similar_to`** | `ähnlich_wie`, `vergleichbar` | Synonym / Ähnlichkeit. | +| **`references`** | *(Kein Alias)* | Standard-Verweis (Fallback). | \ No newline at end of file