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