erster Aufschlag WP22

This commit is contained in:
Lars 2025-12-18 11:50:52 +01:00
parent 77a6db7e92
commit e2ee5df815
7 changed files with 313 additions and 106 deletions

View File

@ -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"

View File

@ -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:

View File

@ -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()

View File

@ -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.

View File

@ -47,7 +47,7 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio
| **WP-19** | Graph Visualisierung | **Frontend Modularisierung:** Umbau auf `ui_*.py`.<br>**Graph Engines:** Parallelbetrieb von Cytoscape (COSE) und Agraph.<br>**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

View File

@ -254,3 +254,65 @@ Bitte bestätige die Übernahme, erstelle die `edge_mappings.yaml` Struktur und
* 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!
## 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.

View File

@ -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). |