diff --git a/docs/00_Dokumentations_Index.md b/docs/00_Dokumentations_Index.md index 15afe47..28adf43 100644 --- a/docs/00_Dokumentations_Index.md +++ b/docs/00_Dokumentations_Index.md @@ -19,13 +19,14 @@ ### Spezialisierte Referenzen 6. **[06_Konfigurationsdateien_Referenz.md](./06_Konfigurationsdateien_Referenz.md)** - Config-Dateien Format & Aufbau -7. **[07_Event_Handler_Commands.md](./07_Event_Handler_Commands.md)** - Event Handler & Commands +7. **[Interview_Config_Guide.md](./Interview_Config_Guide.md)** - Vollständige Anleitung für `interview_config.yaml` (inkl. WP-26 Features, GenAI-freundlich) +8. **[07_Event_Handler_Commands.md](./07_Event_Handler_Commands.md)** - Event Handler & Commands ### WP-26 Integration (Section Types & Intra-Note-Edges) -8. **[06_LH_WP26_Plugin_Integration.md](./06_LH_WP26_Plugin_Integration.md)** - Lastenheft für WP-26 Plugin-Integration (vollständige Anforderungen) -9. **[WP26_Plugin_Interface_Specification.md](./WP26_Plugin_Interface_Specification.md)** - Vollständige Schnittstellenspezifikation für Plugin-Entwicklung -10. **[WP26_Implementation_Checklist.md](./WP26_Implementation_Checklist.md)** - Implementierungs-Checkliste mit Tasks und Phasen +9. **[06_LH_WP26_Plugin_Integration.md](./06_LH_WP26_Plugin_Integration.md)** - Lastenheft für WP-26 Plugin-Integration (vollständige Anforderungen) +10. **[WP26_Plugin_Interface_Specification.md](./WP26_Plugin_Interface_Specification.md)** - Vollständige Schnittstellenspezifikation für Plugin-Entwicklung +11. **[WP26_Implementation_Checklist.md](./WP26_Implementation_Checklist.md)** - Implementierungs-Checkliste mit Tasks und Phasen ### Chain Inspector Reports @@ -118,7 +119,7 @@ - [x] **edge_vocabulary.md** - Format, Parsing-Regeln, Beispiel - [x] **graph_schema.md** - Format, Parsing-Regeln, Beispiel -- [x] **interview_config.yaml** - Format, Felder, Beispiel (Profile, Steps, Loops) +- [x] **interview_config.yaml** - Format, Felder, Beispiel (Profile, Steps, Loops) - **Siehe auch:** [Interview_Config_Guide.md](./Interview_Config_Guide.md) für vollständige Anleitung - [x] **chain_roles.yaml** - Format, Felder, Beispiel (Roles, Edge Types) - [x] **chain_templates.yaml** - Format, Felder, Beispiel (Templates, Slots, Links, Defaults, Profiles) - [x] **analysis_policies.yaml** - Geplante Struktur (noch nicht vollständig implementiert) @@ -167,7 +168,8 @@ ### Konfiguration → [02_Administratorhandbuch.md](./02_Administratorhandbuch.md) -→ [06_Konfigurationsdateien_Referenz.md](./06_Konfigurationsdateien_Referenz.md) +→ [06_Konfigurationsdateien_Referenz.md](./06_Konfigurationsdateien_Referenz.md) +→ [Interview_Config_Guide.md](./Interview_Config_Guide.md) - Interview-Profile erstellen (GenAI-freundlich) ### Nutzung → [01_Benutzerhandbuch.md](./01_Benutzerhandbuch.md) diff --git a/docs/06_Konfigurationsdateien_Referenz.md b/docs/06_Konfigurationsdateien_Referenz.md index 40cd45b..ae9a709 100644 --- a/docs/06_Konfigurationsdateien_Referenz.md +++ b/docs/06_Konfigurationsdateien_Referenz.md @@ -175,41 +175,22 @@ Definiert Profile, Steps, Loops für Note-Erstellung und Interviews. **YAML-Datei** mit strukturierter Hierarchie. -### Struktur +### Vollständige Dokumentation -```yaml -version: "2.0" -frontmatter_whitelist: - - tags - - status +**→ [Interview_Config_Guide.md](./Interview_Config_Guide.md)** - Vollständige Anleitung mit: +- Alle Step-Typen (`capture_frontmatter`, `capture_text`, `capture_text_line`, `loop`, `review`, etc.) +- WP-26 Features (Section Types, Block-IDs, Referenzen) +- Beispiele für verschiedene Note-Typen (experience, insight, decision, principle) +- Best Practices & Patterns +- GenAI-Prompt-Template -profiles: - - key: experience_basic - label: "Erfahrung (Basis)" - note_type: experience - description: "Basic experience profile" - defaults: - folder: "experiences" - tags: ["experience"] - steps: - - id: context - prompt: "Beschreibe den Kontext" - input_type: textarea - required: true - - id: details - prompt: "Weitere Details" - input_type: textarea - required: false - post_run: - edger: true -``` - -### Felder +### Kurzübersicht #### Root-Level -- **`version`** (string, optional): Config-Version (Standard: "2.0") -- **`frontmatter_whitelist`** (array of strings, optional): Erlaubte Frontmatter-Keys (zusätzlich zu Standard) +- **`version`** (string, optional): Config-Version (aktuell: `3`) +- **`frontmatter_whitelist`** (array of strings, optional): Erlaubte Frontmatter-Keys +- **`ui_defaults`** (object, optional): Standard-UI-Einstellungen - **`profiles`** (array, required): Liste von Profilen #### Profile @@ -217,72 +198,66 @@ profiles: - **`key`** (string, required): Eindeutiger Profil-Schlüssel - **`label`** (string, required): Anzeige-Name - **`note_type`** (string, required): Note-Type (z.B. `experience`, `insight`, `decision`) -- **`description`** (string, optional): Beschreibung -- **`defaults`** (object, optional): Standardwerte - - **`folder`** (string, optional): Standard-Ordner - - **`tags`** (array of strings, optional): Standard-Tags - - Weitere Felder möglich -- **`steps`** (array, optional): Interview-Steps -- **`post_run`** (object, optional): Post-Run-Actions - - **`edger`** (boolean, optional): Semantic Mapping Builder ausführen +- **`group`** (string, optional): Gruppierung für UI +- **`defaults`** (object, optional): Standardwerte (status, folder, chunking_profile, etc.) +- **`edging`** (object, optional): Semantic Mapping Konfiguration (`mode`, `wrapperCalloutType`, etc.) +- **`steps`** (array, required): Liste von Steps -#### Step +#### Step-Typen -- **`id`** (string, required): Eindeutige Step-ID -- **`prompt`** (string, required): Prompt-Text -- **`input_type`** (string, required): Input-Typ (`text`, `textarea`, `select`, etc.) -- **`required`** (boolean, optional): Ob Step erforderlich ist -- **`default`** (string, optional): Standardwert -- **`options`** (array, optional): Optionen für `select` Input-Type -- **`loop`** (object, optional): Loop-Konfiguration - - **`key`** (string, required): Loop-Key - - **`prompt`** (string, required): Loop-Prompt - - **`min_items`** (number, optional): Minimale Anzahl Items - - **`max_items`** (number, optional): Maximale Anzahl Items - - **`nested_loops`** (array, optional): Verschachtelte Loops +- **`capture_frontmatter`**: Frontmatter-Feld erfassen +- **`capture_text`**: Mehrzeiligen Text erfassen (mit Section-Header) +- **`capture_text_line`**: Einzeiligen Text erfassen (optional mit Heading-Level) +- **`loop`**: Wiederholbare Steps für Listen +- **`review`**: Review & Apply Step +- **`instruction`**: Anweisung anzeigen +- **`llm_dialog`**: LLM-Dialog (experimentell) +- **`entity_picker`**: Entity-Auswahl (experimentell) + +#### WP-26 Features (nur für `capture_text` und `capture_text_line`) + +- **`section_type`** (string, optional): Section-Type (überschreibt `note_type`) +- **`block_id`** (string, optional): Explizite Block-ID +- **`generate_block_id`** (boolean, optional): Automatische Block-ID-Generierung +- **`references`** (array, optional): Explizite Referenzen zu vorherigen Sections ### Beispiel ```yaml -version: "2.0" -frontmatter_whitelist: - - tags - - status +version: 3 profiles: - key: experience_basic + group: experience label: "Erfahrung (Basis)" note_type: experience - description: "Basic experience profile" defaults: - folder: "experiences" - tags: ["experience"] + status: active + folder: "03_experience" + edging: + mode: both steps: - - id: context - prompt: "Beschreibe den Kontext" - input_type: textarea + - id: title + kind: capture_frontmatter + field: title + label: "Titel" required: true - - id: events - prompt: "Wichtige Ereignisse" - input_type: textarea - loop: - key: events - prompt: "Ereignis" - min_items: 1 - max_items: 10 - post_run: - edger: true - - key: insight_basic - label: "Einsicht (Basis)" - note_type: insight - defaults: - folder: "insights" - steps: - - id: insight - prompt: "Beschreibe die Einsicht" - input_type: textarea + - id: context + kind: capture_text + section: "## 📖 Kontext" + label: "Kontext" required: true + prompt: "Beschreibe den Kontext" + section_type: experience + generate_block_id: true + + - id: review + kind: review + label: "Review & Apply" + checks: + - lint_current_note + - missing_targets ``` ### Verwendung @@ -290,7 +265,21 @@ profiles: - **Profil-Auswahl:** Profile werden in ProfileSelectionModal angezeigt - **Wizard:** Steps werden als Wizard-Steps angezeigt - **Frontmatter:** Frontmatter wird mit Whitelist generiert -- **Post-Run:** Actions werden nach Wizard ausgeführt +- **WP-26:** Section-Types und Block-IDs werden für Edge-Vorschläge verwendet + +### Wann erscheint der Dialog „Verbindungen bearbeiten“ (Sektions-Links)? + +Der Dialog zum **Ändern der Sektions-Verbindungen** (Section-Edges) erscheint beim Klick auf **„Apply & Finish“** im **Review**-Step – aber nur wenn **beides** erfüllt ist: + +1. **Mindestens zwei Sections im Profil** + Das gewählte Interview-Profil muss **mindestens zwei Steps** haben, die eine **Section** erzeugen: + - **`capture_text`** mit Feld **`section`** (z. B. `section: "## 📖 Kontext"`). + - **`capture_text_line`** mit **`heading_level.enabled: true`** und (**`block_id`** oder **`generate_block_id: true`**). + +2. **Edge-Vocabulary geladen** + Die Datei **`edge_vocabulary.md`** muss existieren und der Pfad in den Plugin-Einstellungen stimmen (z. B. `_system/dictionary/edge_vocabulary.md`). + +**Beispiel:** Ein Profil mit drei `capture_text`-Steps mit je `section` (z. B. Kontext, Auslöser, Transformation) erzeugt drei Sections → der Dialog erscheint. Optional können Sie **`section_type`**, **`block_id`** oder **`generate_block_id`** setzen, damit Block-IDs und Edge-Vorschläge aus dem Graph-Schema genutzt werden. --- diff --git a/docs/Interview_Config_Guide.md b/docs/Interview_Config_Guide.md new file mode 100644 index 0000000..7b93924 --- /dev/null +++ b/docs/Interview_Config_Guide.md @@ -0,0 +1,857 @@ +# Interview Config Guide - Vollständige Anleitung für `interview_config.yaml` + +> **Version:** 1.0.0 +> **Stand:** 2025-01-25 +> **Zielgruppe:** Administratoren, GenAI-Assistenten, Plugin-Entwickler +> **Zweck:** Vollständige Dokumentation zur Erstellung von Interview-Profilen für verschiedene Note-Typen + +--- + +## Inhaltsverzeichnis + +1. [Überblick](#überblick) +2. [Grundstruktur](#grundstruktur) +3. [Root-Level Felder](#root-level-felder) +4. [Profile-Definition](#profile-definition) +5. [Step-Typen](#step-typen) +6. [WP-26 Features (Section Types & Block-IDs)](#wp-26-features-section-types--block-ids) +7. [Beispiele für verschiedene Note-Typen](#beispiele-für-verschiedene-note-typen) +8. [Best Practices & Patterns](#best-practices--patterns) +9. [Validierung & Fehlerbehandlung](#validierung--fehlerbehandlung) + +--- + +## Überblick + +Die `interview_config.yaml` definiert **Interview-Profile** für die strukturierte Erstellung von Notes im Obsidian Vault. Jedes Profil beschreibt einen **Wizard-Flow** mit mehreren **Steps**, die nacheinander durchlaufen werden, um eine Note zu erstellen. + +### Wichtige Konzepte + +- **Profile:** Ein Interview-Profil entspricht einem Note-Typ (z.B. `experience`, `insight`, `decision`) +- **Steps:** Einzelne Schritte im Interview-Wizard (z.B. Text-Eingabe, Frontmatter-Feld, Loop) +- **Loops:** Wiederholbare Steps für Listen von Items +- **Section Types (WP-26):** Typisierung von Abschnitten innerhalb einer Note +- **Block-IDs (WP-26):** Eindeutige Referenzpunkte für Intra-Note-Verlinkungen + +--- + +## Grundstruktur + +```yaml +version: 3 + +frontmatter_whitelist: + - id + - title + - type + - status + # ... weitere erlaubte Frontmatter-Keys + +ui_defaults: + modal: + width: "clamp(720px, 88vw, 1100px)" + height: "clamp(640px, 86vh, 920px)" + editor: + preview_toggle: true + toolbar: true + full_width_inputs: true + +profiles: + - key: profile_key + label: "Anzeige-Name" + note_type: experience + # ... Profile-Definition +``` + +--- + +## Root-Level Felder + +### `version` (string, optional) + +**Standard:** `3` + +Die Versionsnummer der Config-Datei. Aktuell wird Version `3` verwendet. + +### `frontmatter_whitelist` (array of strings, optional) + +Liste von Frontmatter-Keys, die im Wizard bearbeitet werden können. + +**Standard-Keys (immer verfügbar):** +- `id` +- `title` +- `type` +- `status` +- `retriever_weight` +- `chunking_profile` +- `tags` +- `aliases` +- `created` +- `interview_profile` + +**Beispiel:** +```yaml +frontmatter_whitelist: + - custom_field + - priority +``` + +### `ui_defaults` (object, optional) + +Standard-UI-Einstellungen für den Interview-Wizard. + +**Felder:** +- `modal.width` (string): CSS-Width für das Modal +- `modal.height` (string): CSS-Height für das Modal +- `editor.preview_toggle` (boolean): Preview-Toggle anzeigen +- `editor.toolbar` (boolean): Toolbar anzeigen +- `editor.full_width_inputs` (boolean): Volle Breite für Inputs + +### `profiles` (array, **required**) + +Liste von Interview-Profilen. Siehe [Profile-Definition](#profile-definition). + +--- + +## Profile-Definition + +Jedes Profil definiert einen vollständigen Interview-Wizard für einen Note-Typ. + +### Pflichtfelder + +- **`key`** (string, required): Eindeutiger Schlüssel für das Profil (z.B. `experience_cluster`) +- **`label`** (string, required): Anzeige-Name im UI +- **`note_type`** (string, required): Note-Typ (muss mit `types.yaml` übereinstimmen, z.B. `experience`, `insight`, `decision`) +- **`steps`** (array, required): Liste von Steps (mindestens ein Step) + +### Optionale Felder + +- **`group`** (string, optional): Gruppierung für UI (z.B. `experience`, `insight`) +- **`defaults`** (object, optional): Standardwerte für Frontmatter + - `status` (string): Standard-Status (z.B. `active`, `stable`) + - `folder` (string): Standard-Ordner für neue Notes + - `chunking_profile` (string): Standard-Chunking-Profil + - `retriever_weight` (number): Standard-Retriever-Weight + - `tags` (array of strings): Standard-Tags +- **`edging`** (object, optional): Semantic Mapping Konfiguration + - `mode` (string): `"none"` | `"post_run"` | `"inline_micro"` | `"both"` (Standard: `"none"`) + - `wrapperCalloutType` (string, optional): Override für Wrapper-Callout-Typ + - `wrapperTitle` (string, optional): Override für Wrapper-Titel + - `wrapperFolded` (boolean, optional): Override für Wrapper-Folded-State + +**Beispiel:** +```yaml +profiles: + - key: experience_basic + group: experience + label: "Erfahrung (Basis)" + note_type: experience + defaults: + status: active + folder: "03_experience" + chunking_profile: timeline + edging: + mode: both + steps: + # ... Steps +``` + +--- + +## Step-Typen + +### 1. `capture_frontmatter` - Frontmatter-Feld erfassen + +Erfasst ein einzelnes Frontmatter-Feld. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"capture_frontmatter"` +- `field` (string, required): Frontmatter-Feld-Name (muss in `frontmatter_whitelist` sein) +- `label` (string, optional): Anzeige-Name (Standard: `field`) +- `required` (boolean, optional): Ob erforderlich (Standard: `false`) +- `prompt` (string, optional): Prompt-Text für den Benutzer +- `input.kind` (string, optional): Input-Typ (`text_line`, `number`, `select`, etc.) +- `input.min` (number, optional): Min-Wert für `number` +- `input.max` (number, optional): Max-Wert für `number` +- `input.step` (number, optional): Step-Wert für `number` +- `input.options` (array, optional): Optionen für `select` + +**Beispiel:** +```yaml +- id: title + kind: capture_frontmatter + field: title + label: "Titel" + required: true + prompt: "Gib einen Titel für die Note ein" + +- id: retriever_weight + kind: capture_frontmatter + field: retriever_weight + label: "Retriever Weight" + required: false + input: + kind: number + min: -3 + max: 3 + step: 1 +``` + +### 2. `capture_text` - Mehrzeiligen Text erfassen + +Erfasst mehrzeiligen Text und erzeugt eine Section in der Note. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"capture_text"` +- `label` (string, optional): Anzeige-Name +- `required` (boolean, optional): Ob erforderlich (Standard: `false`) +- `prompt` (string, optional): Prompt-Text für den Benutzer +- `section` (string, optional): Markdown-Section-Header (z.B. `"## 📖 Kontext"`) + - Wenn leer (`""`), wird kein Section-Header erzeugt (z.B. in Loops) +- **WP-26 Felder:** Siehe [WP-26 Features](#wp-26-features-section-types--block-ids) + +**Beispiel:** +```yaml +- id: context + kind: capture_text + section: "## 📖 Kontext" + label: "Kontext" + required: true + prompt: "Beschreibe den Kontext der Erfahrung" + section_type: experience + generate_block_id: true +``` + +### 3. `capture_text_line` - Einzeiligen Text erfassen + +Erfasst einzeiligen Text, optional mit Heading-Level. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"capture_text_line"` +- `label` (string, optional): Anzeige-Name +- `required` (boolean, optional): Ob erforderlich (Standard: `false`) +- `prompt` (string, optional): Prompt-Text für den Benutzer +- `heading_level.enabled` (boolean, optional): Heading-Level-Selector anzeigen (Standard: `false`) +- `heading_level.default` (number, optional): Standard-Heading-Level 1-6 (Standard: `2`) +- **WP-26 Felder:** Siehe [WP-26 Features](#wp-26-features-section-types--block-ids) + +**Beispiel:** +```yaml +- id: subtitle + kind: capture_text_line + label: "Untertitel" + heading_level: + enabled: true + default: 1 + prompt: "Kurzer Untertitel" + section_type: experience + generate_block_id: true +``` + +### 4. `loop` - Wiederholbare Steps + +Erzeugt eine Liste von Items, die durch wiederholte Ausführung der Sub-Steps erzeugt werden. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"loop"` +- `label` (string, optional): Anzeige-Name für die Loop +- `item_label` (string, optional): Anzeige-Name für einzelne Items (z.B. `"Erlebnis"`) +- `min_items` (number, optional): Minimale Anzahl Items (Standard: `0`) +- `max_items` (number, optional): Maximale Anzahl Items (kein Limit, wenn nicht gesetzt) +- `steps` (array, required): Liste von Sub-Steps (werden für jedes Item ausgeführt) +- `output.join` (string, optional): String zum Verbinden der Items (Standard: `"\n\n"`) +- `ui.mode` (string, optional): UI-Modus (`subwizard` | `inline`, Standard: `inline`) +- `ui.commit` (string, optional): Commit-Modus (`explicit_add` | `on_next`, Standard: `explicit_add`) +- `ui.allow_edit` (boolean, optional): Bearbeitung erlauben (Standard: `false`) +- `ui.allow_delete` (boolean, optional): Löschen erlauben (Standard: `false`) +- `ui.allow_reorder` (boolean, optional): Neuordnen erlauben (Standard: `false`) +- `ui.show_item_overview` (boolean, optional): Item-Übersicht anzeigen (Standard: `false`) + +**Beispiel:** +```yaml +- id: actions + kind: loop + label: "Handlungen" + item_label: "Handlung" + min_items: 1 + steps: + - id: action_heading + kind: capture_text_line + label: "Handlungsüberschrift" + required: true + heading_level: + enabled: true + default: 3 + section_type: decision + generate_block_id: true + - id: action_description + kind: capture_text + section: "" + label: "Beschreibung" + required: true +``` + +**Verschachtelte Loops:** +```yaml +- id: groups + kind: loop + label: "Erlebnis-Gruppen" + steps: + - id: group_heading + kind: capture_text_line + label: "Überschrift" + - id: entries + kind: loop + label: "Einträge" + steps: + - id: entry + kind: capture_text_line + label: "Listeneintrag" +``` + +### 5. `review` - Review & Apply Step + +Zeigt eine Vorschau der generierten Note und ermöglicht abschließende Checks. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"review"` +- `label` (string, optional): Anzeige-Name (Standard: `"Review & Apply"`) +- `checks` (array of strings, optional): Liste von Checks + - `lint_current_note`: Lint-Check für die generierte Note + - `missing_targets`: Prüft auf fehlende Link-Targets + - `missing_frontmatter_id`: Prüft auf fehlende Frontmatter-ID + +**Beispiel:** +```yaml +- id: review + kind: review + label: "Review & Apply" + checks: + - lint_current_note + - missing_targets + - missing_frontmatter_id +``` + +### 6. `instruction` - Anweisung anzeigen + +Zeigt eine reine Anweisung ohne Input. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"instruction"` +- `label` (string, optional): Anzeige-Name +- `content` (string, required): Markdown-Inhalt der Anweisung + +**Beispiel:** +```yaml +- id: intro + kind: instruction + label: "Einführung" + content: | + # Willkommen zum Interview + + Dieses Interview hilft dir, eine strukturierte Note zu erstellen. +``` + +### 7. `llm_dialog` - LLM-Dialog (experimentell) + +Öffnet einen LLM-Dialog für Text-Generierung oder -Verdichtung. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"llm_dialog"` +- `label` (string, optional): Anzeige-Name +- `mode` (string, optional): `"manual"` | `"auto"` (Standard: `"manual"`) +- `prompt_template` (string, required): Prompt-Template für den LLM +- `output_target.kind` (string, required): Output-Ziel (`section_append` | `replace`) +- `output_target.section` (string, optional): Section für `section_append` + +**Beispiel:** +```yaml +- id: llm_refine + kind: llm_dialog + label: "LLM – Verdichtung" + mode: manual + prompt_template: | + Verdichte die bisherigen Inhalte zu 3-5 Bulletpoints. + Erfinde keine Fakten. + output_target: + kind: section_append + section: "## 🧠 Verdichtung (LLM Vorschlag)" +``` + +### 8. `entity_picker` - Entity-Auswahl (experimentell) + +Ermöglicht die Auswahl einer bestehenden Note aus dem Vault. + +**Felder:** +- `id` (string, required): Eindeutige Step-ID +- `kind` (string, required): `"entity_picker"` +- `label` (string, optional): Anzeige-Name +- `prompt` (string, optional): Prompt-Text +- `required` (boolean, optional): Ob erforderlich (Standard: `false`) +- `labelField` (string, optional): Feld-Key für Wikilink-Label + +--- + +## WP-26 Features (Section Types & Block-IDs) + +**Wichtig:** Diese Features sind nur für `capture_text` und `capture_text_line` Steps verfügbar. + +### `section_type` (string, optional) + +Definiert den **Section-Type** für diese Section. Überschreibt den `note_type` für diese Section. + +**Verwendung:** +- Für **Edge-Type-Vorschläge** basierend auf `graph_schema.md` +- Für **Type-Boost-Scoring** im Retriever +- Für **Agentic Validation** (Phase 3) + +**Beispiel:** +```yaml +- id: insight + kind: capture_text + section: "## 💡 Einsicht" + section_type: insight # Typ-Wechsel: experience -> insight +``` + +**Fallback-Logik:** +- Wenn `section_type` nicht gesetzt ist, wird der `note_type` des Profils verwendet +- Bei verschachtelten Sections wird der Section-Type der übergeordneten Section verwendet (basierend auf Heading-Level) + +### `block_id` (string, optional) + +Definiert eine **explizite Block-ID** für diese Section. Die Block-ID wird als `^block-id` am Ende der Überschrift erzeugt. + +**Beispiel:** +```yaml +- id: situation + kind: capture_text + section: "## ⚡ Situation" + block_id: "sit" # Erzeugt: ## ⚡ Situation ^sit +``` + +**Wichtig:** Block-IDs müssen **eindeutig** sein. In Loops werden automatisch Nummerierungen angehängt (z.B. `action_heading-1`, `action_heading-2`). + +### `generate_block_id` (boolean, optional) + +Wenn `true`, wird automatisch eine Block-ID aus der Step-ID generiert (slugified). + +**Beispiel:** +```yaml +- id: context + kind: capture_text + section: "## 📖 Kontext" + generate_block_id: true # Erzeugt: ## 📖 Kontext ^context +``` + +**Regeln:** +- Wenn sowohl `block_id` als auch `generate_block_id: true` gesetzt sind, hat `block_id` Priorität +- In Loops wird automatisch eine Nummerierung angehängt (z.B. `context-1`, `context-2`) + +### `references` (array, optional) + +Definiert **explizite Referenzen** zu vorherigen Sections (Block-IDs). + +**Struktur:** +```yaml +references: + - block_id: + edge_type: +``` + +**Beispiel:** +```yaml +- id: situation + kind: capture_text + section: "## ⚡ Situation" + section_type: experience + block_id: "sit" + references: + - block_id: context + edge_type: derived_from # Explizite Referenz zu vorheriger Section +``` + +**Verhalten:** +- Referenzen werden als **Intra-Note-Edges** erzeugt (`is_internal: true`) +- Der `edge_type` wird als **Vorschlag** verwendet (kann im `SectionEdgesOverviewModal` geändert werden) +- Wenn kein `edge_type` angegeben ist, wird ein Typ aus `graph_schema.md` vorgeschlagen +- **Automatische Rückwärts-Edges** werden in der Ziel-Section erzeugt (basierend auf dem inversen Edge-Type) + +**Wichtig:** `block_id` muss auf eine **vorherige Section** verweisen (nicht auf zukünftige Sections). + +--- + +## Beispiele für verschiedene Note-Typen + +### Experience (Erfahrung) + +```yaml +- key: experience_single + group: experience + label: "Experience – Einzelereignis" + note_type: experience + defaults: + status: active + chunking_profile: timeline + edging: + mode: both + steps: + - id: title + kind: capture_frontmatter + field: title + label: "Titel" + required: true + + - id: context + kind: capture_text + section: "## 📖 Kontext" + label: "Kontext" + required: true + prompt: "In welchem Rahmen ist es passiert?" + section_type: experience + generate_block_id: true + + - id: trigger + kind: capture_text + section: "## ⚡ Auslöser" + label: "Auslöser" + required: false + prompt: "Was hat es ausgelöst?" + section_type: experience + generate_block_id: true + references: + - block_id: context + edge_type: derived_from + + - id: transformation + kind: capture_text + section: "## 🧠 Innere Transformation" + label: "Innere Transformation" + required: false + prompt: "Was hat sich innerlich verändert?" + section_type: insight # Typ-Wechsel + generate_block_id: true + + - id: review + kind: review + label: "Review & Apply" + checks: + - lint_current_note + - missing_targets +``` + +### Insight (Einsicht) + +```yaml +- key: insight_basic + group: insight + label: "Insight – Basis" + note_type: insight + defaults: + status: active + folder: "04_insight" + edging: + mode: both + steps: + - id: title + kind: capture_frontmatter + field: title + label: "Titel" + required: true + + - id: insight + kind: capture_text + section: "## 💡 Einsicht" + label: "Einsicht" + required: true + prompt: "Beschreibe die Einsicht" + section_type: insight + generate_block_id: true + + - id: source + kind: capture_text + section: "## 📚 Quelle" + label: "Quelle" + required: false + prompt: "Woher stammt diese Einsicht?" + section_type: insight + generate_block_id: true + references: + - block_id: insight + edge_type: source_of + + - id: application + kind: capture_text + section: "## 🎯 Anwendung" + label: "Anwendung" + required: false + prompt: "Wie wird diese Einsicht angewendet?" + section_type: decision # Typ-Wechsel + generate_block_id: true + references: + - block_id: insight + edge_type: foundation_for + + - id: review + kind: review + label: "Review & Apply" + checks: + - lint_current_note +``` + +### Decision (Entscheidung) + +```yaml +- key: decision_basic + group: decision + label: "Decision – Basis" + note_type: decision + defaults: + status: active + folder: "05_decision" + edging: + mode: both + steps: + - id: title + kind: capture_frontmatter + field: title + label: "Titel" + required: true + + - id: context + kind: capture_text + section: "## 📖 Kontext" + label: "Kontext" + required: true + prompt: "In welchem Kontext wurde die Entscheidung getroffen?" + section_type: experience + generate_block_id: true + + - id: options + kind: loop + label: "Optionen" + item_label: "Option" + min_items: 2 + steps: + - id: option_text + kind: capture_text + section: "" + label: "Option" + required: true + prompt: "Beschreibe eine Option" + + - id: decision + kind: capture_text + section: "## 🎯 Entscheidung" + label: "Entscheidung" + required: true + prompt: "Welche Entscheidung wurde getroffen?" + section_type: decision + generate_block_id: true + references: + - block_id: context + edge_type: derived_from + + - id: rationale + kind: capture_text + section: "## 🧠 Begründung" + label: "Begründung" + required: false + prompt: "Warum wurde diese Entscheidung getroffen?" + section_type: insight + generate_block_id: true + references: + - block_id: decision + edge_type: based_on + + - id: review + kind: review + label: "Review & Apply" + checks: + - lint_current_note + - missing_targets +``` + +### Principle (Prinzip) + +```yaml +- key: principle_basic + group: principle + label: "Principle – Basis" + note_type: principle + defaults: + status: stable + chunking_profile: principle_dense + retriever_weight: 2 + steps: + - id: title + kind: capture_frontmatter + field: title + label: "Titel" + required: true + + - id: statement + kind: capture_text + section: "## 🧭 Prinzip" + label: "Prinzip" + required: true + prompt: "Formuliere das Prinzip als klaren Satz." + section_type: principle + generate_block_id: true + + - id: retriever_weight + kind: capture_frontmatter + field: retriever_weight + label: "Retriever Weight" + required: false + input: + kind: number + min: -3 + max: 3 + step: 1 + + - id: review + kind: review + label: "Review & Apply" + checks: + - lint_current_note +``` + +--- + +## Best Practices & Patterns + +### 1. Section-Type-Wechsel dokumentieren + +Wenn ein Section-Type-Wechsel stattfindet, sollte dies im Kommentar dokumentiert werden: + +```yaml +- id: transformation + kind: capture_text + section: "## 🧠 Innere Transformation" + section_type: insight # Typ-Wechsel: experience -> insight + generate_block_id: true +``` + +### 2. Block-IDs konsistent benennen + +- Verwende **kurze, aussagekräftige** Block-IDs (z.B. `sit`, `ctx`, `ins`) +- In Loops werden automatisch Nummerierungen angehängt +- Vermeide **kollidierende** Block-IDs innerhalb eines Profils + +### 3. Referenzen zu vorherigen Sections + +- Referenzen müssen auf **vorherige Sections** verweisen (nicht zukünftige) +- Verwende **explizite `edge_type`-Vorschläge**, wenn die Beziehung klar ist +- Lasse `edge_type` weg, wenn das System aus `graph_schema.md` vorschlagen soll + +### 4. Loops für Listen verwenden + +- Verwende `loop` für **wiederholbare Items** (z.B. Handlungen, Optionen, Erlebnisse) +- Setze `min_items` für **erforderliche Listen** +- Verwende `section: ""` für Steps innerhalb von Loops, die keine eigene Section erzeugen sollen + +### 5. Review-Step immer am Ende + +- Jedes Profil sollte mit einem `review` Step enden +- Aktiviere relevante Checks (`lint_current_note`, `missing_targets`, etc.) + +### 6. Edging-Mode konfigurieren + +- `mode: "both"` für vollständige Edge-Unterstützung (inline + post-run) +- `mode: "inline_micro"` für nur Inline-Edge-Vorschläge +- `mode: "post_run"` für nur Post-Run-Edge-Abfrage +- `mode: "none"` für keine Edge-Unterstützung + +### 7. Frontmatter-Felder dokumentieren + +- Dokumentiere **Custom-Frontmatter-Felder** in `frontmatter_whitelist` +- Verwende **sinnvolle Defaults** in `defaults` für bessere UX + +--- + +## Validierung & Fehlerbehandlung + +### YAML-Syntax-Fehler + +- **Syntax-Fehler** werden beim Laden erkannt und geloggt +- **Last-Known-Good:** Letzte gültige Config wird bei Fehlern verwendet +- **Warnings:** Ungültige Felder werden als Warnings geloggt, nicht als Fehler + +### Step-Validierung + +- **Fehlende Pflichtfelder:** Werden als Fehler erkannt +- **Ungültige Step-Kinds:** Werden als Warnings geloggt +- **Ungültige Block-ID-Referenzen:** Werden zur Laufzeit erkannt (keine Validierung zur Config-Zeit) + +### Live-Reload + +- **Debounced:** 200ms Delay für Performance +- **Automatisch:** Bei Dateiänderungen +- **Manuell:** Über Commands möglich + +--- + +## GenAI-Prompt-Template + +Wenn du ein GenAI-Assistent bist und ein neues Interview-Profil erstellen sollst, verwende folgendes Template: + +``` +Erstelle ein Interview-Profil für den Note-Typ "{note_type}" mit folgenden Anforderungen: + +1. **Profil-Informationen:** + - Key: {profile_key} + - Label: {display_name} + - Group: {group} + - Note-Type: {note_type} + +2. **Default-Werte:** + - Status: {status} + - Folder: {folder} + - Chunking-Profile: {chunking_profile} (optional) + - Retriever-Weight: {retriever_weight} (optional) + +3. **Steps:** + {step_descriptions} + +4. **WP-26 Features:** + - Section-Types: {section_types} + - Block-IDs: {block_ids} + - Referenzen: {references} + +5. **Edging:** + - Mode: {edging_mode} + +6. **Review:** + - Checks: {review_checks} + +Nutze die Beispiele aus der Dokumentation als Vorlage und stelle sicher, dass: +- Alle Pflichtfelder gesetzt sind +- Block-IDs eindeutig sind +- Referenzen auf vorherige Sections verweisen +- Section-Types konsistent mit graph_schema.md sind +- Ein Review-Step am Ende steht +``` + +--- + +## Zusammenfassung + +Die `interview_config.yaml` ist eine mächtige Konfigurationsdatei für die strukturierte Erstellung von Notes. Mit WP-26 Features können Sections typisiert und referenziert werden, was zu besseren Edge-Vorschlägen und Retrieval-Ergebnissen führt. + +**Wichtigste Punkte:** +1. Jedes Profil benötigt `key`, `label`, `note_type` und `steps` +2. WP-26 Features (`section_type`, `block_id`, `generate_block_id`, `references`) sind nur für `capture_text` und `capture_text_line` verfügbar +3. Block-IDs müssen eindeutig sein (automatische Nummerierung in Loops) +4. Referenzen müssen auf vorherige Sections verweisen +5. Ein Review-Step sollte immer am Ende stehen + +--- + +**Ende der Interview Config Guide** diff --git a/docs/audit_geburtsdatei.md b/docs/audit_geburtsdatei.md new file mode 100644 index 0000000..d862081 --- /dev/null +++ b/docs/audit_geburtsdatei.md @@ -0,0 +1,146 @@ +# Audit: Geburt unserer Kinder Rouven und Rohan.md + +## Datei-Analyse + +### Section-Sequenz: +1. **Kontext** (`experience`) - Block-ID: `context` +2. **Situation** (`experience`) - Block-ID: `sit` +3. **Emotionen** (`experience`) - Block-ID: `emotions` +4. **Einsicht** (`insight`) - Block-ID: `insight` +5. **Entscheidung** (`decision`) - Block-ID: `decision` +6. **G1** (`decision`) - Block-ID: `action_heading-1` (Loop-Item) +7. **G 2** (`decision`) - Block-ID: `action_heading-2` (Loop-Item) +8. **Reflexion** (`insight`) - Block-ID: `reflection` + +--- + +## Gefundene Probleme + +### 1. ❌ FEHLENDE Forward-Edges zwischen Sections + +**Problem:** Forward-Edges zwischen aufeinanderfolgenden Sections fehlen komplett. + +**Erwartet:** +- Situation sollte Forward-Edge von Kontext haben (`experience` → `experience`: `related_to` oder `references`) +- Emotionen sollte Forward-Edge von Situation haben (`experience` → `experience`: `related_to` oder `references`) +- Einsicht sollte Forward-Edge von Emotionen haben (`experience` → `insight`: `resulted_in`) +- Entscheidung sollte Forward-Edge von Einsicht haben (`insight` → `decision`: `foundation_for`) +- G1 sollte Forward-Edge von Entscheidung haben (`decision` → `decision`: `related_to` oder `references`) +- G 2 sollte Forward-Edge von Entscheidung und G1 haben (`decision` → `decision`: `related_to` oder `references`) +- Reflexion sollte Forward-Edge von Entscheidung haben (`decision` → `insight`: nicht explizit definiert, sollte `related_to` sein) + +**Aktuell:** Keine automatischen Forward-Edges zwischen Sections vorhanden. + +--- + +### 2. ❌ FEHLENDE Backward-Edges in Ziel-Sections + +**Problem:** Backward-Edges fehlen komplett in den Ziel-Sections. + +**Erwartet:** +- Kontext sollte Backward-Edge von Situation haben (inverse von `related_to` = `related_to`) +- Situation sollte Backward-Edge von Emotionen haben (inverse von `related_to` = `related_to`) +- Emotionen sollte Backward-Edge von Einsicht haben (inverse von `resulted_in` = `caused_by`) +- Einsicht sollte Backward-Edge von Entscheidung haben (inverse von `foundation_for` = `based_on`) +- Entscheidung sollte Backward-Edge von G1 haben (inverse von `related_to` = `related_to`) +- G1 sollte Backward-Edge von G 2 haben (inverse von `related_to` = `related_to`) +- Entscheidung sollte Backward-Edge von Reflexion haben (inverse von `related_to` = `related_to`) + +**Aktuell:** Keine automatischen Backward-Edges vorhanden. + +--- + +### 3. ⚠️ FALSCHE Edge-Types in bestehenden Edges + +**Problem:** Viele Edge-Types entsprechen nicht dem graph_schema.md. + +#### Kontext-Section: +- `referenced_by` → `decision`: ❌ Falsch, sollte `references` sein (experience → decision: `references` oder `related_to`) +- `referenced_by` → `emotions`: ❌ Falsch, sollte `references` sein (experience → experience: `references` oder `related_to`) +- `referenced_by` → `sit`: ❌ Falsch, sollte `references` sein (experience → experience: `references` oder `related_to`) +- `caused_by` → `insight`: ❌ Falsch, sollte `resulted_in` sein (experience → insight: `resulted_in`) +- `caused_by` → `reflection`: ❌ Falsch, sollte `resulted_in` sein (experience → insight: `resulted_in`) + +#### Situation-Section: +- `derived_from` → `context`: ✅ Korrekt (experience → experience: `references` oder `related_to`, `derived_from` ist akzeptabel) +- `referenced_by` → `decision`: ❌ Falsch +- `referenced_by` → `emotions`: ❌ Falsch +- `caused_by` → `insight`: ❌ Falsch +- `caused_by` → `reflection`: ❌ Falsch + +#### Emotionen-Section: +- `references` → `context`: ✅ Korrekt +- `references` → `sit`: ✅ Korrekt +- `referenced_by` → `decision`: ❌ Falsch +- `caused_by` → `insight`: ❌ Falsch +- `caused_by` → `reflection`: ❌ Falsch + +#### Einsicht-Section: +- `resulted_in` → `context`: ❌ Falsch, sollte `caused_by` sein (insight → experience: nicht explizit, aber `caused_by` ist logisch) +- `resulted_in` → `emotions`: ❌ Falsch +- `resulted_in` → `sit`: ❌ Falsch +- `based_on` → `decision`: ✅ Korrekt (insight → decision: `foundation_for`, inverse = `based_on`) +- `referenced_by` → `reflection`: ❌ Falsch + +#### Entscheidung-Section: +- `references` → `context`: ✅ Korrekt +- `references` → `emotions`: ✅ Korrekt +- `references` → `sit`: ✅ Korrekt +- `foundation_for` → `insight`: ✅ Korrekt (decision → insight: nicht explizit, aber `foundation_for` ist logisch) +- `resulted_in` → `reflection`: ❌ Falsch, sollte `foundation_for` sein (decision → insight: nicht explizit, aber `foundation_for` ist logischer) + +#### G1-Section (Loop-Item): +- `caused_by` → `action_heading`: ❌ Falsch, Block-ID `action_heading` existiert nicht (sollte `action_heading-1` oder `action_heading-2` sein) +- `caused_by` → `decision`: ❌ Falsch, sollte `based_on` sein (decision → decision: `related_to` oder `references`) +- `references` → `context`: ✅ Korrekt +- `references` → `emotions`: ✅ Korrekt +- `references` → `sit`: ✅ Korrekt +- `foundation_for` → `insight`: ✅ Korrekt +- `foundation_for` → `reflection`: ✅ Korrekt + +#### G 2-Section (Loop-Item): +- `caused_by` → `action_heading`: ❌ Falsch, Block-ID `action_heading` existiert nicht +- `caused_by` → `action_heading-1`: ✅ Korrekt (decision → decision: `related_to` oder `references`) +- `caused_by` → `decision`: ❌ Falsch, sollte `based_on` sein +- `references` → `context`: ✅ Korrekt +- `references` → `emotions`: ✅ Korrekt +- `references` → `sit`: ✅ Korrekt +- `foundation_for` → `insight`: ✅ Korrekt +- `foundation_for` → `reflection`: ✅ Korrekt + +#### Reflexion-Section: +- `caused_by` → `action_heading`: ❌ Falsch, Block-ID `action_heading` existiert nicht +- `caused_by` → `decision`: ❌ Falsch, sollte `based_on` sein (insight → decision: `foundation_for`, inverse = `based_on`) +- `resulted_in` → `context`: ❌ Falsch +- `resulted_in` → `emotions`: ❌ Falsch +- `resulted_in` → `sit`: ❌ Falsch +- `builds_on` → `insight`: ❌ Falsch, sollte `based_on` sein (insight → insight: nicht explizit, aber `based_on` ist logisch) + +--- + +### 4. ❌ FEHLENDE Block-ID-Referenzen + +**Problem:** Referenzen auf nicht-existierende Block-IDs: +- `action_heading` wird referenziert, existiert aber nicht (sollte `action_heading-1` oder `action_heading-2` sein) + +--- + +### 5. ✅ Abstract Wrapper vorhanden + +**Status:** Alle Sections haben einen `> [!abstract]` Wrapper. ✅ + +--- + +## Zusammenfassung + +### Kritische Probleme: +1. ❌ **Keine automatischen Forward-Edges** zwischen aufeinanderfolgenden Sections +2. ❌ **Keine automatischen Backward-Edges** in Ziel-Sections +3. ❌ **Viele falsche Edge-Types** die nicht dem graph_schema.md entsprechen +4. ❌ **Referenzen auf nicht-existierende Block-IDs** (`action_heading`) + +### Empfehlungen: +1. Interview-Wizard sollte automatisch Forward-Edges zwischen Sections generieren +2. Interview-Wizard sollte automatisch Backward-Edges in Ziel-Sections generieren +3. Edge-Types sollten gegen graph_schema.md validiert werden +4. Block-ID-Referenzen sollten validiert werden diff --git a/src/interview/renderer.test.ts b/src/interview/renderer.test.ts index 6879e7a..2809c5c 100644 --- a/src/interview/renderer.test.ts +++ b/src/interview/renderer.test.ts @@ -46,8 +46,6 @@ describe("WP-26 Interview Renderer", () => { it("sollte keine Selbstreferenz bei automatischen Edges generieren", () => { const profile: InterviewProfile = { - version: "2.0", - frontmatterWhitelist: [], key: "test", label: "Test", note_type: "experience", @@ -80,7 +78,7 @@ describe("WP-26 Interview Renderer", () => { const result = renderProfileToMarkdown(profile, answers, mockOptions); // Prüfe, dass keine Selbstreferenz vorhanden ist - const contextSection = result.match(/## Kontext.*?## Einsicht/s)?.[0]; + const contextSection = result.match(/## Kontext[\s\S]*?## Einsicht/)?.[0]; expect(contextSection).toBeDefined(); // Prüfe, dass keine Edge auf sich selbst zeigt @@ -94,8 +92,6 @@ describe("WP-26 Interview Renderer", () => { it("sollte Edge-Types aus graph_schema verwenden", () => { const profile: InterviewProfile = { - version: "2.0", - frontmatterWhitelist: [], key: "test", label: "Test", note_type: "experience", @@ -150,8 +146,6 @@ describe("WP-26 Interview Renderer", () => { it("sollte Backlinks automatisch generieren", () => { const profile: InterviewProfile = { - version: "2.0", - frontmatterWhitelist: [], key: "test", label: "Test", note_type: "experience", @@ -193,8 +187,6 @@ describe("WP-26 Interview Renderer", () => { it("sollte keine Selbstreferenz bei expliziten Referenzen erlauben", () => { const profile: InterviewProfile = { - version: "2.0", - frontmatterWhitelist: [], key: "test", label: "Test", note_type: "experience", @@ -232,8 +224,6 @@ describe("WP-26 Interview Renderer", () => { it("sollte Section-Type-Callout direkt nach Heading platzieren", () => { const profile: InterviewProfile = { - version: "2.0", - frontmatterWhitelist: [], key: "test", label: "Test", note_type: "experience", @@ -258,14 +248,12 @@ describe("WP-26 Interview Renderer", () => { const result = renderProfileToMarkdown(profile, answers, mockOptions); // Prüfe, dass Section-Type-Callout direkt nach Heading steht - const sectionPattern = /## Kontext.*?\n> \[!section\] experience/s; + const sectionPattern = /## Kontext[\s\S]*?\n> \[!section\] experience/; expect(result).toMatch(sectionPattern); }); it("sollte Edges am Ende der Sektion platzieren", () => { const profile: InterviewProfile = { - version: "2.0", - frontmatterWhitelist: [], key: "test", label: "Test", note_type: "experience", @@ -298,7 +286,7 @@ describe("WP-26 Interview Renderer", () => { const result = renderProfileToMarkdown(profile, answers, mockOptions); // Prüfe, dass Edges am Ende der Einsicht-Sektion stehen (nach dem Text) - const insightSection = result.match(/## Einsicht.*?Einsicht-Text\n\n(> \[!edge\].*)/s)?.[1]; + const insightSection = result.match(/## Einsicht[\s\S]*?Einsicht-Text\n\n(> \[!edge\][\s\S]*)/)?.[1]; expect(insightSection).toBeDefined(); expect(insightSection).toMatch(/> \[!edge\]/); }); diff --git a/src/interview/renderer.ts b/src/interview/renderer.ts index 0a87738..ddcfe49 100644 --- a/src/interview/renderer.ts +++ b/src/interview/renderer.ts @@ -23,6 +23,7 @@ export interface RenderOptions { graphSchema?: GraphSchema | null; vocabulary?: Vocabulary | null; noteType?: string; // Fallback für effective_type + sectionEdgeTypes?: Map>; // fromBlockId -> (toBlockId -> edgeType) für manuell geänderte Edge-Types } /** @@ -35,13 +36,20 @@ export function renderProfileToMarkdown( answers: RenderAnswers, options?: RenderOptions ): string { - const output: string[] = []; - // WP-26: Verwende übergebene Section-Sequenz oder erstelle neue // Die Section-Sequenz wird während des interaktiven Wizard-Durchlaufs getrackt const sectionSequence: SectionInfo[] = answers.sectionSequence ? [...answers.sectionSequence] : []; const generatedBlockIds = new Map(); + console.log(`[WP-26] renderProfileToMarkdown:`, { + sectionSequenceLength: sectionSequence.length, + sectionSequence: sectionSequence.map(s => ({ + stepKey: s.stepKey, + blockId: s.blockId, + sectionType: s.sectionType, + })), + }); + // WP-26: Wenn Section-Sequenz übergeben wurde, fülle generatedBlockIds aus der Sequenz if (answers.sectionSequence && answers.sectionSequence.length > 0) { for (const sectionInfo of answers.sectionSequence) { @@ -51,24 +59,125 @@ export function renderProfileToMarkdown( } } - // WP-26: Track Section-Sequenz während des Renderns (für Steps, die noch nicht getrackt wurden) + const context: RenderContext = { + profile, + sectionSequence, + generatedBlockIds, + options: options || {}, + }; + + // WP-26: Erster Durchlauf: Rendere alle Steps und sammle gerenderte Sections + const renderedSections: Array<{ sectionInfo: SectionInfo; content: string }> = []; + const output: string[] = []; + for (const step of profile.steps) { - const stepOutput = renderStep( - step, - answers, - { - profile, - sectionSequence, - generatedBlockIds, - options: options || {}, - } - ); + // WP-26: Debug-Log für Steps mit WP-26 Feldern + if (step.type === "capture_text" || step.type === "capture_text_line") { + const captureStep = step as CaptureTextStep | CaptureTextLineStep; + console.log(`[WP-26] Renderer: Step ${step.key} hat WP-26 Felder:`, { + section_type: captureStep.section_type, + block_id: captureStep.block_id, + generate_block_id: captureStep.generate_block_id, + references: captureStep.references, + }); + } + + const stepOutput = renderStep(step, answers, context); + if (stepOutput) { + // WP-26: Prüfe, ob diese Step eine Section mit Block-ID erzeugt hat + if (step.type === "capture_text" || step.type === "capture_text_line") { + const captureStep = step as CaptureTextStep | CaptureTextLineStep; + let blockId: string | null = null; + if (captureStep.block_id) { + blockId = captureStep.block_id; + } else if (captureStep.generate_block_id) { + blockId = slugify(captureStep.key); + } + + if (blockId && generatedBlockIds.has(blockId)) { + const sectionInfo = generatedBlockIds.get(blockId)!; + renderedSections.push({ sectionInfo, content: stepOutput }); + } + } + output.push(stepOutput); } } - return output.join("\n\n").trim(); + // WP-26: Zweiter Durchlauf: Füge Backward-Edges zu den entsprechenden Sections hinzu + // Backward-Edges werden in der Ziel-Sektion (vorherige Section) eingefügt + // Wenn Section A -> Section B (Forward-Edge), dann wird Backward-Edge (B -> A) in Section A eingefügt + // Spezifikation: Zu jedem Forward-Link wird automatisch ein Rückwärts-Edge in der Ziel-Section generiert + const updatedOutput: string[] = []; + const backwardEdgesMap = new Map(); // blockId der prevSection -> backwardEdges + + // Sammle Backward-Edges für ALLE vorherigen Sections + // Für jede aktuelle Section generiere Backward-Edges zu allen vorherigen Sections + for (const rendered of renderedSections) { + const currentIndex = context.sectionSequence.findIndex( + s => s.blockId === rendered.sectionInfo.blockId && s.stepKey === rendered.sectionInfo.stepKey + ); + + if (currentIndex > 0 && rendered.sectionInfo.blockId) { + // ALLE vorherigen Sections (nicht nur die direkt vorherige) + const prevSections = context.sectionSequence.slice(0, currentIndex); + + // Für jede vorherige Section generiere eine Backward-Edge + for (let prevIdx = 0; prevIdx < prevSections.length; prevIdx++) { + const prevSection = prevSections[prevIdx]; + if (!prevSection || !prevSection.blockId) { + continue; + } + + const backwardEdge = renderSingleBackwardEdge( + rendered.sectionInfo, + prevSection, + currentIndex, + prevIdx, + context + ); + if (backwardEdge) { + const existing = backwardEdgesMap.get(prevSection.blockId) || ""; + const combined = existing ? `${existing}\n${backwardEdge}` : backwardEdge; + backwardEdgesMap.set(prevSection.blockId, combined); + } + } + } + } + + // Füge Backward-Edges zu den entsprechenden Sections hinzu + for (let i = 0; i < output.length; i++) { + const currentOutput = output[i]; + if (!currentOutput) { + updatedOutput.push(""); + continue; + } + + // Prüfe, ob diese Output eine Section mit Block-ID ist + let sectionInfo: SectionInfo | null = null; + for (const rendered of renderedSections) { + if (rendered.content === currentOutput) { + sectionInfo = rendered.sectionInfo; + break; + } + } + + if (sectionInfo && sectionInfo.blockId) { + const backwardEdges = backwardEdgesMap.get(sectionInfo.blockId); + if (backwardEdges) { + // Füge Backward-Edges zum abstract wrapper hinzu + const updatedContent = addEdgesToAbstractWrapper(currentOutput, backwardEdges); + updatedOutput.push(updatedContent); + } else { + updatedOutput.push(currentOutput); + } + } else { + updatedOutput.push(currentOutput); + } + } + + return updatedOutput.join("\n\n").trim(); } // WP-26: Render-Kontext für Section-Types und Block-IDs @@ -126,6 +235,17 @@ function renderCaptureText( const text = String(value); + console.log(`[WP-26] renderCaptureText für Step ${step.key}:`, { + hasBlockId: !!step.block_id, + blockId: step.block_id, + hasGenerateBlockId: !!step.generate_block_id, + generateBlockId: step.generate_block_id, + hasSectionType: !!step.section_type, + sectionType: step.section_type, + hasSection: !!step.section, + section: step.section, + }); + // WP-26: Block-ID-Generierung let blockId: string | null = null; if (step.block_id) { @@ -134,6 +254,8 @@ function renderCaptureText( blockId = slugify(step.key); } + console.log(`[WP-26] Generierte Block-ID für Step ${step.key}:`, blockId); + // WP-26: Heading mit Block-ID erweitern let heading = step.section || ""; if (heading && blockId) { @@ -158,6 +280,13 @@ function renderCaptureText( // WP-26: Block-ID tracken if (blockId) { context.generatedBlockIds.set(blockId, sectionInfo); + + // WP-26: Auch Basis-Block-ID tracken (für Referenzen ohne Index bei Loop-Items) + // Z.B. "action_heading-1" wird auch als "action_heading" getrackt + const baseBlockId = blockId.replace(/-\d+$/, ""); + if (baseBlockId !== blockId && !context.generatedBlockIds.has(baseBlockId)) { + context.generatedBlockIds.set(baseBlockId, sectionInfo); + } } // WP-26: Section-Sequenz aktualisieren (nur wenn Section vorhanden) @@ -165,11 +294,15 @@ function renderCaptureText( context.sectionSequence.push(sectionInfo); } - // WP-26: Referenzen generieren (am Ende der Sektion) + // WP-26: Referenzen generieren (Forward-Edges zu anderen Sections) const references = renderReferences(step.references || [], context, sectionInfo); - // WP-26: Automatische Edge-Vorschläge generieren (am Ende der Sektion) - const autoEdges = renderAutomaticEdges(sectionInfo, context); + // WP-26: Automatische Forward-Edges generieren (prevSection -> currentSection) + const forwardEdges = renderForwardEdges(sectionInfo, context); + + // WP-26: Automatische Backward-Edges generieren (currentSection -> nachfolgende Sections) + // Diese werden später in einem zweiten Durchlauf hinzugefügt + const backwardEdges = ""; // Wird später hinzugefügt // Use template if provided if (step.output?.template) { @@ -180,12 +313,12 @@ function renderCaptureText( }); // WP-26: Template mit Heading, Section-Type und Edges kombinieren - return combineSectionParts(heading, sectionTypeCallout, templateText, references, autoEdges); + return combineSectionParts(heading, sectionTypeCallout, templateText, references, forwardEdges, backwardEdges); } // Default: use section if provided, otherwise just the text if (step.section) { - return combineSectionParts(heading, sectionTypeCallout, text, references, autoEdges); + return combineSectionParts(heading, sectionTypeCallout, text, references, forwardEdges, backwardEdges); } return text; @@ -223,6 +356,7 @@ function renderCaptureTextLine( } // WP-26: Block-ID-Generierung + // Für Loop-Items kann die Block-ID bereits nummeriert sein (z.B. "action_heading-1") let blockId: string | null = null; if (step.block_id) { blockId = step.block_id; @@ -245,28 +379,9 @@ function renderCaptureTextLine( ? `> [!section] ${step.section_type}` : ""; - // WP-26: Section-Info für Tracking (nur wenn Heading vorhanden) - if (headingPrefix) { - const sectionInfo: SectionInfo = { - stepKey: step.key, - sectionType: step.section_type || null, - heading: heading || text, - blockId: blockId, - noteType: context.options.noteType || context.profile.note_type, - }; - - // WP-26: Block-ID tracken - if (blockId) { - context.generatedBlockIds.set(blockId, sectionInfo); - } - - // WP-26: Section-Sequenz aktualisieren - context.sectionSequence.push(sectionInfo); - } - // WP-26: Section-Info für Tracking (nur wenn Heading vorhanden) let sectionInfo: SectionInfo | null = null; - if (headingPrefix) { + if (headingPrefix && blockId) { sectionInfo = { stepKey: step.key, sectionType: step.section_type || null, @@ -275,23 +390,28 @@ function renderCaptureTextLine( noteType: context.options.noteType || context.profile.note_type, }; - // WP-26: Block-ID tracken - if (blockId) { - context.generatedBlockIds.set(blockId, sectionInfo); + // WP-26: Block-ID tracken (auch wenn nummeriert für Loop-Items) + context.generatedBlockIds.set(blockId, sectionInfo); + + // WP-26: Auch Basis-Block-ID tracken (für Referenzen ohne Index) + // Z.B. "action_heading-1" wird auch als "action_heading" getrackt + const baseBlockId = blockId.replace(/-\d+$/, ""); + if (baseBlockId !== blockId && !context.generatedBlockIds.has(baseBlockId)) { + context.generatedBlockIds.set(baseBlockId, sectionInfo); } // WP-26: Section-Sequenz aktualisieren context.sectionSequence.push(sectionInfo); } - // WP-26: Referenzen generieren (am Ende der Sektion) + // WP-26: Referenzen generieren (Forward-Edges zu anderen Sections) const references = sectionInfo ? renderReferences(step.references || [], context, sectionInfo) : ""; - // WP-26: Automatische Edge-Vorschläge generieren (nur wenn Heading vorhanden) - const autoEdges = sectionInfo - ? renderAutomaticEdges(sectionInfo, context) + // WP-26: Automatische Forward-Edges generieren (prevSection -> currentSection) + const forwardEdges = sectionInfo + ? renderForwardEdges(sectionInfo, context) : ""; // Use template if provided @@ -305,7 +425,8 @@ function renderCaptureTextLine( // WP-26: Template mit Heading, Section-Type und Edges kombinieren (wenn Heading vorhanden) if (headingPrefix) { - return combineSectionParts(heading, sectionTypeCallout, "", references, autoEdges); + // Backward-Edges werden später im zweiten Durchlauf hinzugefügt + return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, ""); } return templateText; @@ -313,7 +434,8 @@ function renderCaptureTextLine( // WP-26: Wenn Heading vorhanden, mit Section-Type und Edges kombinieren if (headingPrefix) { - return combineSectionParts(heading, sectionTypeCallout, "", references, autoEdges); + // Backward-Edges werden später im zweiten Durchlauf hinzugefügt + return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, ""); } // Default: text with heading prefix if configured @@ -401,11 +523,15 @@ function renderLoopRecursive( const itemOutputs: string[] = []; + // WP-26: Tracke Item-Index für eindeutige Block-IDs + let itemIndex = 0; + for (const item of items) { if (!item || typeof item !== "object") { continue; } + itemIndex++; const itemParts: string[] = []; // Render each nested step for this item (recursively) @@ -417,8 +543,16 @@ function renderLoopRecursive( const text = String(fieldValue); const captureStep = nestedStep as CaptureTextStep; + // WP-26: Erstelle temporären Step mit nummerierter Block-ID für Loop-Items + const loopStepWithBlockId: CaptureTextStep = { + ...captureStep, + block_id: captureStep.generate_block_id + ? `${slugify(captureStep.key)}-${itemIndex}` + : captureStep.block_id, + }; + // WP-26: Verwende erweiterte renderCaptureText Funktion - const rendered = renderCaptureText(captureStep, { + const rendered = renderCaptureText(loopStepWithBlockId, { collectedData: new Map([[nestedStep.key, fieldValue]]), loopContexts: answers.loopContexts, }, context); @@ -442,8 +576,16 @@ function renderLoopRecursive( itemData.set(headingLevelKey, headingLevel); } + // WP-26: Erstelle temporären Step mit nummerierter Block-ID für Loop-Items + const loopStepWithBlockId: CaptureTextLineStep = { + ...captureStep, + block_id: captureStep.generate_block_id + ? `${slugify(captureStep.key)}-${itemIndex}` + : captureStep.block_id, + }; + // WP-26: Verwende erweiterte renderCaptureTextLine Funktion - const rendered = renderCaptureTextLine(captureStep, { + const rendered = renderCaptureTextLine(loopStepWithBlockId, { collectedData: itemData, loopContexts: answers.loopContexts, }, context); @@ -537,14 +679,15 @@ function renderTemplate(template: string, tokens: { text: string; field: string; /** * Kombiniert Heading, Section-Type-Callout, Content und Edges zu einer Section. - * Formatierungsregel: Section-Type direkt nach Heading, Edges am Ende. + * Formatierungsregel: Section-Type direkt nach Heading, Edges im abstract wrapper am Ende. */ function combineSectionParts( heading: string, sectionTypeCallout: string, content: string, references: string, - autoEdges: string + autoEdges: string, + backwardEdges: string = "" ): string { const parts: string[] = []; @@ -560,18 +703,97 @@ function combineSectionParts( parts.push(content); } - // WP-26: Edges am Ende der Sektion (Referenzen + automatische Edges) - const edges = [references, autoEdges].filter(e => e.trim()).join("\n"); - if (edges) { - parts.push(edges); + // WP-26: Alle Edges im abstract wrapper am Ende der Sektion + const allEdges = [references, autoEdges, backwardEdges].filter(e => e.trim()); + if (allEdges.length > 0) { + const abstractWrapper = buildAbstractWrapper(allEdges.join("\n")); + if (abstractWrapper) { + parts.push(abstractWrapper); + } } return parts.join("\n\n"); } +/** + * Erstellt einen abstract wrapper für Edges. + * Format: > [!abstract]- 🕸️ Semantic Mapping\n>> [!edge] type\n>> [[link]] + */ +function buildAbstractWrapper(edgesContent: string): string | null { + if (!edgesContent.trim()) { + return null; + } + + // Parse edges aus dem Content + const edgeLines = edgesContent.split("\n").filter(line => line.trim()); + const edgeGroups = new Map(); + + let currentEdgeType: string | null = null; + for (const line of edgeLines) { + // Prüfe auf Edge-Type: > [!edge] type + const edgeMatch = line.match(/^>\s*\[!edge\]\s+(.+)$/); + if (edgeMatch && edgeMatch[1]) { + currentEdgeType = edgeMatch[1].trim(); + if (!edgeGroups.has(currentEdgeType)) { + edgeGroups.set(currentEdgeType, []); + } + } else { + // Prüfe auf Link: > [[#^block-id]] + const linkMatch = line.match(/^>\s*(\[\[#\^.+?\]\])$/); + if (linkMatch && linkMatch[1] && currentEdgeType) { + const links = edgeGroups.get(currentEdgeType) || []; + links.push(linkMatch[1]); + edgeGroups.set(currentEdgeType, links); + } + } + } + + if (edgeGroups.size === 0) { + return null; + } + + // Build abstract wrapper + const wrapperLines: string[] = []; + wrapperLines.push(`> [!abstract]- 🕸️ Semantic Mapping`); + + // Sort edge types alphabetically + const sortedEdgeTypes = Array.from(edgeGroups.keys()).sort(); + + for (let i = 0; i < sortedEdgeTypes.length; i++) { + const edgeType = sortedEdgeTypes[i]; + if (!edgeType) continue; + const links = edgeGroups.get(edgeType) || []; + + if (links.length === 0) { + continue; + } + + // Add separator between groups (except before first) + if (i > 0) { + wrapperLines.push(">"); + } + + // Edge header + wrapperLines.push(`>> [!edge] ${edgeType}`); + + // Links (sorted for determinism) + const sortedLinks = [...links].sort(); + for (const link of sortedLinks) { + wrapperLines.push(`>> ${link}`); + } + } + + // Add block-id marker + wrapperLines.push(`> ^map-${Date.now()}`); + + return wrapperLines.join("\n"); +} + /** * Generiert Edge-Callouts für explizite Referenzen. * Format: > [!edge] \n> [[#^block-id]] + * + * WP-26: Unterstützt auch Basis-Block-IDs für Loop-Items (z.B. "action_heading" wird zu "action_heading-1", "action_heading-2" aufgelöst) */ function renderReferences( references: Array<{ block_id: string; edge_type?: string }>, @@ -585,35 +807,98 @@ function renderReferences( const edgeCallouts: string[] = []; for (const ref of references) { - // Prüfe, ob Block-ID existiert + // WP-26: Auflösung der Block-ID (für Loop-Items mit Basis-Block-ID) + let resolvedBlockId = ref.block_id; + + // Prüfe, ob Block-ID direkt existiert if (!context.generatedBlockIds.has(ref.block_id)) { - console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`); - continue; + // Versuche, Block-ID mit Index aufzulösen (für Loop-Items) + // Suche nach Block-IDs, die mit der Basis-Block-ID beginnen + let foundBlockId: string | null = null; + for (const [blockId] of context.generatedBlockIds.entries()) { + // Prüfe, ob Block-ID mit Basis-Block-ID beginnt (z.B. "action_heading-1", "action_heading-2") + if (blockId.startsWith(`${ref.block_id}-`)) { + // Verwende die neueste gefundene Block-ID (höchster Index) + if (!foundBlockId || blockId > foundBlockId) { + foundBlockId = blockId; + } + } + } + + if (foundBlockId) { + resolvedBlockId = foundBlockId; + } else { + console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`); + continue; + } } // WP-26: Prüfe auf Selbstreferenz - if (ref.block_id === currentSection.blockId) { - console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${ref.block_id}" zeigt auf sich selbst`); + if (resolvedBlockId === currentSection.blockId) { + console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${resolvedBlockId}" zeigt auf sich selbst`); continue; } const edgeType = ref.edge_type || "related_to"; edgeCallouts.push(`> [!edge] ${edgeType}`); - edgeCallouts.push(`> [[#^${ref.block_id}]]`); + edgeCallouts.push(`> [[#^${resolvedBlockId}]]`); } return edgeCallouts.join("\n"); } /** - * Generiert automatische Edge-Vorschläge zwischen Sections. - * Verwendet graph_schema.md für typische Edge-Types und edge_vocabulary.md für inverse Edges. - * - * Formatierungsregel: Edges werden am Ende der aktuellen Section eingefügt. - * Forward-Edge: prevSection -> currentSection (in aktueller Section) - * Rückwärts-Edge: currentSection -> prevSection (in aktueller Section, inverser Edge-Type) + * Ermittelt den effektiven Section-Type basierend auf Heading-Level. + * Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section + * auf dem gleichen oder höheren Level verwendet, sonst Note-Type. */ -function renderAutomaticEdges( +function getEffectiveSectionType( + section: SectionInfo, + index: number, + sectionSequence: SectionInfo[] +): string { + // Wenn expliziter Section-Type vorhanden, verwende diesen + if (section.sectionType) { + return section.sectionType; + } + + // Extrahiere Heading-Level aus der Überschrift + const headingMatch = section.heading.match(/^(#{1,6})\s+/); + const currentLevel = headingMatch ? headingMatch[1]?.length || 0 : 0; + + // Suche rückwärts nach der letzten Section mit explizitem Type auf gleichem oder höherem Level + for (let i = index - 1; i >= 0; i--) { + const prevSection = sectionSequence[i]; + if (!prevSection) continue; + + const prevHeadingMatch = prevSection.heading.match(/^(#{1,6})\s+/); + const prevLevel = prevHeadingMatch ? prevHeadingMatch[1]?.length || 0 : 0; + + // Wenn vorherige Section auf gleichem oder höherem Level einen expliziten Type hat + if (prevLevel <= currentLevel && prevSection.sectionType) { + return prevSection.sectionType; + } + + // Wenn wir auf ein höheres Level stoßen, stoppe die Suche + if (prevLevel < currentLevel) { + break; + } + } + + // Fallback: Note-Type + return section.noteType; +} + +/** + * Generiert automatische Forward-Edges zwischen Sections. + * Forward-Edge: prevSection -> currentSection (in aktueller Section) + * Generiert Edges zu ALLEN vorherigen Sections (nicht nur zur direkt vorherigen). + * Verwendet graph_schema.md für typische Edge-Types. + * + * Spezifikation: Alle Sections haben Edges zu allen anderen Sections. + * Edge-Types werden aus graph_schema.md abgeleitet. + */ +function renderForwardEdges( currentSection: SectionInfo, context: RenderContext ): string { @@ -622,43 +907,158 @@ function renderAutomaticEdges( return ""; } - // Nur wenn es vorherige Sections gibt - if (context.sectionSequence.length <= 1) { - return ""; + // Finde Index der aktuellen Section in der Sequenz + const currentIndex = context.sectionSequence.findIndex( + s => s.blockId === currentSection.blockId && s.stepKey === currentSection.stepKey + ); + + if (currentIndex === -1 || currentIndex === 0) { + return ""; // Keine vorherigen Sections } - // Aktuelle Section ist die letzte in der Sequenz - // Wir generieren Edges von allen vorherigen Sections zur aktuellen Section - const prevSections = context.sectionSequence.slice(0, -1); + // ALLE vorherigen Sections (nicht nur die direkt vorherige) + const prevSections = context.sectionSequence.slice(0, currentIndex); const edgeCallouts: string[] = []; - const { graphSchema, vocabulary } = context.options; + const { graphSchema } = context.options; - for (const prevSection of prevSections) { + for (let prevIdx = 0; prevIdx < prevSections.length; prevIdx++) { + const prevSection = prevSections[prevIdx]; // Nur wenn vorherige Section auch eine Block-ID hat - if (!prevSection.blockId) { + if (!prevSection || !prevSection.blockId) { continue; } - // WP-26: Prüfe auf Selbstreferenz (sollte nicht vorkommen, aber sicherheitshalber prüfen) + // WP-26: Prüfe auf Selbstreferenz if (prevSection.blockId === currentSection.blockId) { console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${prevSection.blockId}" zeigt auf sich selbst`); continue; } - // Ermittle effective_types - const prevType = prevSection.sectionType || prevSection.noteType; - const currentType = currentSection.sectionType || currentSection.noteType; + // WP-26: Auflösung der Block-ID für Loop-Items + // Prüfe zuerst, ob die Block-ID direkt existiert + let resolvedPrevBlockId = prevSection.blockId; + if (!context.generatedBlockIds.has(prevSection.blockId)) { + // Versuche, Block-ID mit Index aufzulösen (für Loop-Items) + // Suche nach Block-IDs, die mit der Basis-Block-ID beginnen + let foundBlockId: string | null = null; + for (const [blockId] of context.generatedBlockIds.entries()) { + if (blockId.startsWith(`${prevSection.blockId}-`)) { + // Verwende die neueste gefundene Block-ID (höchster Index) + if (!foundBlockId || blockId > foundBlockId) { + foundBlockId = blockId; + } + } + } + + if (foundBlockId) { + resolvedPrevBlockId = foundBlockId; + } + } - // Lookup in graph_schema.md: prevType -> currentType + // WP-26: Prüfe zuerst, ob ein manuell geänderter Edge-Type vorhanden ist let forwardEdgeType: string | null = null; + if (context.options.sectionEdgeTypes) { + const fromBlockId = prevSection.blockId; + const toBlockId = currentSection.blockId; + if (fromBlockId && toBlockId) { + const fromMap = context.options.sectionEdgeTypes.get(fromBlockId); + if (fromMap) { + forwardEdgeType = fromMap.get(toBlockId) || null; + if (forwardEdgeType) { + console.log(`[WP-26] Verwende manuell geänderten Edge-Type: ${fromBlockId} -> ${toBlockId} = ${forwardEdgeType}`); + } + } + } + } + + // Falls kein manuell geänderter Edge-Type vorhanden, verwende graph_schema + if (!forwardEdgeType) { + // WP-26: Ermittle effective_types mit Heading-Level-basierter Fallback-Logik + const prevType = getEffectiveSectionType(prevSection, prevIdx, context.sectionSequence); + const currentType = getEffectiveSectionType(currentSection, currentIndex, context.sectionSequence); + + // Lookup in graph_schema.md: prevType -> currentType + if (graphSchema && prevType && currentType) { + const hints = getHints(graphSchema, prevType, currentType); + if (hints.typical.length > 0) { + forwardEdgeType = hints.typical[0] || null; + } else { + console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}, verwende Fallback`); + } + } + + // Fallback: related_to für experience -> experience, references für andere + if (!forwardEdgeType) { + if (prevType === currentType && prevType === "experience") { + forwardEdgeType = "related_to"; + } else { + forwardEdgeType = "references"; + } + } + } + + // WP-26: Forward-Edge generieren (prevSection -> currentSection) + // Diese Edge wird in der aktuellen Section eingefügt + edgeCallouts.push(`> [!edge] ${forwardEdgeType}`); + edgeCallouts.push(`> [[#^${resolvedPrevBlockId}]]`); + } + + return edgeCallouts.join("\n"); +} + +/** + * Generiert eine einzelne Backward-Edge zwischen zwei Sections. + * Backward-Edge: currentSection -> prevSection (wird in prevSection eingefügt) + * + * Wenn Section A -> Section B (Forward-Edge), dann wird Backward-Edge (B -> A) in Section A eingefügt. + */ +function renderSingleBackwardEdge( + currentSection: SectionInfo, + prevSection: SectionInfo, + currentIndex: number, + prevIndex: number, + context: RenderContext +): string { + // Nur wenn beide Sections Block-IDs haben + if (!currentSection.blockId || !prevSection.blockId) { + return ""; + } + + // WP-26: Prüfe auf Selbstreferenz + if (prevSection.blockId === currentSection.blockId) { + return ""; + } + + const { graphSchema, vocabulary } = context.options; + + // WP-26: Prüfe zuerst, ob ein manuell geänderter Edge-Type vorhanden ist + let forwardEdgeType: string | null = null; + if (context.options.sectionEdgeTypes) { + const fromBlockId = prevSection.blockId; + const toBlockId = currentSection.blockId; + if (fromBlockId && toBlockId) { + const fromMap = context.options.sectionEdgeTypes.get(fromBlockId); + if (fromMap) { + forwardEdgeType = fromMap.get(toBlockId) || null; + if (forwardEdgeType) { + console.log(`[WP-26] Verwende manuell geänderten Edge-Type für Backward-Edge: ${fromBlockId} -> ${toBlockId} = ${forwardEdgeType}`); + } + } + } + } + + // Falls kein manuell geänderter Edge-Type vorhanden, verwende graph_schema + if (!forwardEdgeType) { + // WP-26: Ermittle effective_types mit Heading-Level-basierter Fallback-Logik + const prevType = getEffectiveSectionType(prevSection, prevIndex, context.sectionSequence); + const currentType = getEffectiveSectionType(currentSection, currentIndex, context.sectionSequence); + + // Lookup in graph_schema.md: prevType -> currentType (Forward-Edge-Type) if (graphSchema && prevType && currentType) { const hints = getHints(graphSchema, prevType, currentType); if (hints.typical.length > 0) { - forwardEdgeType = hints.typical[0]; // Erster typischer Edge-Type aus graph_schema - } else { - // Debug: Log wenn keine typischen Edges gefunden wurden - console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}`); + forwardEdgeType = hints.typical[0] || null; } } @@ -666,32 +1066,136 @@ function renderAutomaticEdges( if (!forwardEdgeType) { forwardEdgeType = "related_to"; } - - // WP-26: Automatische Edge-Vorschläge - // Wir generieren beide Richtungen: - // 1. Forward-Edge: prevSection -> currentSection (mit originalem Edge-Type) - // 2. Rückwärts-Edge: currentSection -> prevSection (mit inversem Edge-Type) - // - // Beide Edges werden in der aktuellen Section eingefügt und zeigen zur prevSection - // Der Backlink wird automatisch mitgesetzt - - // Rückwärts-Edge: currentSection -> prevSection (mit inversem Edge-Type) - let inverseEdgeType: string | null = null; - if (vocabulary) { - inverseEdgeType = vocabulary.getInverse(forwardEdgeType); - } - - // Generiere Rückwärts-Edge (currentSection -> prevSection) - if (inverseEdgeType) { - edgeCallouts.push(`> [!edge] ${inverseEdgeType}`); - edgeCallouts.push(`> [[#^${prevSection.blockId}]]`); - } - - // Generiere Forward-Edge als Backlink (prevSection -> currentSection) - // Diese Edge beschreibt die Beziehung von prevSection zu currentSection - edgeCallouts.push(`> [!edge] ${forwardEdgeType}`); - edgeCallouts.push(`> [[#^${prevSection.blockId}]]`); } - return edgeCallouts.join("\n"); + // WP-26: Backward-Edge generieren (currentSection -> prevSection) + // Das ist die inverse Edge zu der Forward-Edge (prevSection -> currentSection) + let backwardEdgeType: string = forwardEdgeType; + if (vocabulary) { + const inverse = vocabulary.getInverse(forwardEdgeType); + if (inverse) { + backwardEdgeType = inverse; + } + } + + // WP-26: Backward-Edge (currentSection -> prevSection) wird in prevSection eingefügt + // Diese Edge zeigt von currentSection zurück zu prevSection + return `> [!edge] ${backwardEdgeType}\n> [[#^${currentSection.blockId}]]`; +} + +/** + * Fügt Edges zu einem bestehenden abstract wrapper hinzu oder erstellt einen neuen. + */ +function addEdgesToAbstractWrapper(content: string, newEdges: string): string { + if (!newEdges.trim()) { + return content; + } + + // Prüfe, ob bereits ein abstract wrapper vorhanden ist + const abstractWrapperMatch = content.match(/(> \[!abstract\][^\n]*\n(?:>>?[^\n]*\n)*> \^map-\d+)/); + + if (abstractWrapperMatch && abstractWrapperMatch[1]) { + // Füge neue Edges zum bestehenden wrapper hinzu + const existingWrapper = abstractWrapperMatch[1]; + const updatedWrapper = mergeEdgesIntoWrapper(existingWrapper, newEdges); + if (abstractWrapperMatch[0]) { + return content.replace(abstractWrapperMatch[0], updatedWrapper); + } + } + + { + // Erstelle neuen abstract wrapper + const abstractWrapper = buildAbstractWrapper(newEdges); + if (abstractWrapper) { + return content.trimEnd() + "\n\n" + abstractWrapper; + } + return content; + } +} + +/** + * Merged neue Edges in einen bestehenden abstract wrapper. + */ +function mergeEdgesIntoWrapper(existingWrapper: string, newEdges: string): string { + // Parse bestehenden wrapper + const lines = existingWrapper.split("\n"); + const wrapperHeader = lines[0] || "> [!abstract]- 🕸️ Semantic Mapping"; + const mapMarker = lines[lines.length - 1] || `> ^map-${Date.now()}`; + + // Parse bestehende Edges + const existingGroups = new Map(); + let currentEdgeType: string | null = null; + + for (let i = 1; i < lines.length - 1; i++) { + const line = lines[i]; + if (!line) continue; + + const edgeMatch = line.match(/^>>\s*\[!edge\]\s+(.+)$/); + if (edgeMatch && edgeMatch[1]) { + currentEdgeType = edgeMatch[1].trim(); + if (!existingGroups.has(currentEdgeType)) { + existingGroups.set(currentEdgeType, []); + } + } else { + const linkMatch = line.match(/^>>\s*(\[\[#\^.+?\]\])$/); + if (linkMatch && linkMatch[1] && currentEdgeType) { + const links = existingGroups.get(currentEdgeType) || []; + links.push(linkMatch[1]); + existingGroups.set(currentEdgeType, links); + } + } + } + + // Parse neue Edges + const newEdgeLines = newEdges.split("\n").filter(line => line.trim()); + for (const line of newEdgeLines) { + const edgeMatch = line.match(/^>\s*\[!edge\]\s+(.+)$/); + if (edgeMatch && edgeMatch[1]) { + currentEdgeType = edgeMatch[1].trim(); + if (!existingGroups.has(currentEdgeType)) { + existingGroups.set(currentEdgeType, []); + } + } else { + const linkMatch = line.match(/^>\s*(\[\[#\^.+?\]\])$/); + if (linkMatch && linkMatch[1] && currentEdgeType) { + const links = existingGroups.get(currentEdgeType) || []; + // Prüfe auf Duplikate + if (!links.includes(linkMatch[1])) { + links.push(linkMatch[1]); + } + existingGroups.set(currentEdgeType, links); + } + } + } + + // Build merged wrapper + const wrapperLines: string[] = []; + wrapperLines.push(wrapperHeader); + + const sortedEdgeTypes = Array.from(existingGroups.keys()).sort(); + + for (let i = 0; i < sortedEdgeTypes.length; i++) { + const edgeType = sortedEdgeTypes[i]; + if (!edgeType) continue; + const links = existingGroups.get(edgeType) || []; + + if (links.length === 0) { + continue; + } + + if (i > 0) { + wrapperLines.push(">"); + } + + wrapperLines.push(`>> [!edge] ${edgeType}`); + + const sortedLinks = [...links].sort(); + for (const link of sortedLinks) { + wrapperLines.push(`>> ${link}`); + } + } + + wrapperLines.push(mapMarker); + + return wrapperLines.join("\n"); } diff --git a/src/interview/wizardState.ts b/src/interview/wizardState.ts index 2067661..c99fbdb 100644 --- a/src/interview/wizardState.ts +++ b/src/interview/wizardState.ts @@ -11,6 +11,15 @@ export interface PendingEdgeAssignment { createdAt: number; } +// WP-26: Section-Info für Block-ID und Section-Type Tracking +export interface SectionInfo { + stepKey: string; + sectionType: string | null; + heading: string; + blockId: string | null; + noteType: string; +} + export interface WizardState { profile: InterviewProfile; currentStepIndex: number; @@ -21,6 +30,9 @@ export interface WizardState { patches: Patch[]; // Collected patches to apply activeLoopPath: string[]; // Stack of loop keys representing current nesting level (e.g. ["items", "item_list"]) pendingEdgeAssignments: PendingEdgeAssignment[]; // Inline micro edge assignments collected during wizard + // WP-26: Section-Type und Block-ID Tracking + generatedBlockIds: Map; + sectionSequence: SectionInfo[]; } export interface Patch { diff --git a/src/mapping/semanticMappingBuilder.ts b/src/mapping/semanticMappingBuilder.ts index 47aa73e..3fd2f63 100644 --- a/src/mapping/semanticMappingBuilder.ts +++ b/src/mapping/semanticMappingBuilder.ts @@ -220,6 +220,19 @@ export async function buildSemanticMappings( // Process each link in worklist for (const item of worklist.items) { + // WP-26: Überspringe automatisch generierte Rückwärts-Edges (Block-ID-Links mit bereits vorhandenem Edge-Type) + // Diese wurden bereits automatisch generiert und müssen nicht nochmals abgefragt werden + const isBlockIdLink = item.link.startsWith("#^"); + if (isBlockIdLink && item.currentType) { + // Block-ID-Link mit bereits vorhandenem Edge-Type = automatisch generierter Rückwärts-Edge + // Verwende den vorhandenen Edge-Type ohne Abfrage + mappingsToUse.set(item.link, item.currentType); + result.existingMappingsKept++; + console.log(`[WP-26] Überspringe automatisch generierten Rückwärts-Edge: ${item.link} -> ${item.currentType}`); + continue; + } + + // Für alle anderen Links (inter-note Links und neue Block-ID-Links ohne Edge-Type) normal abfragen // Always prompt user (even for existing mappings) // Keep is the default option if currentType exists const prompt = new LinkPromptModal(app, item, vocabulary, sourceType, graphSchema); diff --git a/src/ui/EdgeTypeChooserModal.ts b/src/ui/EdgeTypeChooserModal.ts index 268e59b..600fd7e 100644 --- a/src/ui/EdgeTypeChooserModal.ts +++ b/src/ui/EdgeTypeChooserModal.ts @@ -43,7 +43,32 @@ export class EdgeTypeChooserModal extends Modal { contentEl.empty(); contentEl.addClass("edge-type-chooser-modal"); - contentEl.createEl("h2", { text: "Choose edge type" }); + contentEl.createEl("h2", { text: "Edge-Type auswählen" }); + + // WP-26: Zeige Quell- und Ziel-Typ für Übersichtlichkeit + if (this.sourceType || this.targetType) { + const typeInfo = contentEl.createEl("div", { cls: "edge-type-info" }); + typeInfo.style.marginBottom = "1em"; + typeInfo.style.padding = "0.75em"; + typeInfo.style.backgroundColor = "var(--background-modifier-hover)"; + typeInfo.style.borderRadius = "4px"; + + const sourceLabel = typeInfo.createEl("span", { text: "Quell-Typ: " }); + sourceLabel.style.fontWeight = "bold"; + const sourceValue = typeInfo.createEl("span", { + text: this.sourceType || "(unbekannt)" + }); + sourceValue.style.color = "var(--interactive-accent)"; + + typeInfo.createEl("span", { text: " → " }); + + const targetLabel = typeInfo.createEl("span", { text: "Ziel-Typ: " }); + targetLabel.style.fontWeight = "bold"; + const targetValue = typeInfo.createEl("span", { + text: this.targetType || "(unbekannt)" + }); + targetValue.style.color = "var(--interactive-accent)"; + } // Get suggestions const suggestions = computeEdgeSuggestions( diff --git a/src/ui/InlineEdgeTypeModal.ts b/src/ui/InlineEdgeTypeModal.ts index 813bf3f..fb79445 100644 --- a/src/ui/InlineEdgeTypeModal.ts +++ b/src/ui/InlineEdgeTypeModal.ts @@ -9,6 +9,7 @@ import type { EdgeVocabulary } from "../vocab/types"; import type { GraphSchema } from "../mapping/graphSchema"; import type { MindnetSettings } from "../settings"; import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal"; +import { getHints } from "../mapping/graphSchema"; export interface InlineEdgeTypeResult { chosenRawType: string | null; // null means skip @@ -68,6 +69,31 @@ export class InlineEdgeTypeModal extends Modal { }); linkInfo.textContent = `Link: [[${this.linkBasename}]]`; + // WP-26: Zeige Quell- und Ziel-Typ für Übersichtlichkeit + if (this.sourceType || this.targetType) { + const typeInfo = contentEl.createEl("div", { cls: "edge-type-info" }); + typeInfo.style.marginTop = "0.5em"; + typeInfo.style.padding = "0.75em"; + typeInfo.style.backgroundColor = "var(--background-modifier-hover)"; + typeInfo.style.borderRadius = "4px"; + + const sourceLabel = typeInfo.createEl("span", { text: "Quell-Typ: " }); + sourceLabel.style.fontWeight = "bold"; + const sourceValue = typeInfo.createEl("span", { + text: this.sourceType || "(unbekannt)" + }); + sourceValue.style.color = "var(--interactive-accent)"; + + typeInfo.createEl("span", { text: " → " }); + + const targetLabel = typeInfo.createEl("span", { text: "Ziel-Typ: " }); + targetLabel.style.fontWeight = "bold"; + const targetValue = typeInfo.createEl("span", { + text: this.targetType || "(unbekannt)" + }); + targetValue.style.color = "var(--interactive-accent)"; + } + // Show selected type if one was chosen (e.g., from chooser) if (this.selectedEdgeType) { const selectedInfo = contentEl.createEl("div", { @@ -450,34 +476,40 @@ export class InlineEdgeTypeModal extends Modal { const alternatives: string[] = []; const prohibited: string[] = []; - // Try to get hints from graph schema + // WP-26: Verwende getHints() für korrekte Fallback-Logik if (this.graphSchema && this.sourceType && this.targetType) { - const sourceMap = this.graphSchema.schema.get(this.sourceType); - if (sourceMap) { - const hints = sourceMap.get(this.targetType); - if (hints) { - typical.push(...hints.typical); - prohibited.push(...hints.prohibited); - } - } + const hints = getHints(this.graphSchema, this.sourceType, this.targetType); + typical.push(...hints.typical); + prohibited.push(...hints.prohibited); + + console.log(`[WP-26] InlineEdgeTypeModal: Vorschläge für ${this.sourceType} -> ${this.targetType}:`, { + typical: hints.typical, + prohibited: hints.prohibited, + }); } - // If no schema hints, use vocabulary - if (typical.length === 0 && this.vocabulary) { + // Compute alternatives: limit to a reasonable number of common edge types + // Only show alternatives if we have typical types (meaning schema is available) + if (typical.length > 0 && this.vocabulary) { + // Only show alternatives if we have schema-based recommendations + // Limit to first 6-8 most common edge types that aren't typical or prohibited + const allCanonicalTypes = Array.from(this.vocabulary.byCanonical.keys()); + const filtered = allCanonicalTypes.filter(canonical => { + const isTypical = typical.includes(canonical); + const isProhibited = prohibited.includes(canonical); + return !isTypical && !isProhibited; + }); + // Limit to maxAlternatives + const maxAlternatives = this.settings.inlineMaxAlternatives || 6; + alternatives.push(...filtered.slice(0, maxAlternatives)); + } else if (typical.length === 0 && this.vocabulary) { + // Fallback: If no schema hints, use vocabulary // Get top common edge types from vocabulary - // EdgeVocabulary has byCanonical: Map const edgeTypes = Array.from(this.vocabulary.byCanonical.keys()); - // Sort by usage or just take first N const maxAlternatives = this.settings.inlineMaxAlternatives || 6; alternatives.push(...edgeTypes.slice(0, maxAlternatives)); } - // Limit alternatives to maxAlternatives - const maxAlternatives = this.settings.inlineMaxAlternatives || 6; - if (alternatives.length > maxAlternatives) { - alternatives.splice(maxAlternatives); - } - return { typical, alternatives, prohibited }; } } diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index 7ba19c4..78fc2dd 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -37,6 +37,7 @@ import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink"; import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMappingBuilder"; import type { MindnetSettings } from "../settings"; import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal"; +import { SectionEdgesOverviewModal, type SectionEdgesOverviewResult } from "./SectionEdgesOverviewModal"; import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver"; import type { PendingEdgeAssignment } from "../interview/wizardState"; import { VocabularyLoader } from "../vocab/VocabularyLoader"; @@ -186,6 +187,21 @@ export class InterviewWizardModal extends Modal { // WP-26: Track Section-Info während des Wizard-Durchlaufs if (step) { + // Debug: Zeige alle Step-Felder + if (step.type === "capture_text" || step.type === "capture_text_line") { + const isCaptureText = step.type === "capture_text"; + const captureTextStep = isCaptureText ? step as import("../interview/types").CaptureTextStep : null; + const captureTextLineStep = !isCaptureText ? step as import("../interview/types").CaptureTextLineStep : null; + console.log(`[WP-26] Step-Details für ${step.key}:`, { + type: step.type, + section: isCaptureText ? captureTextStep?.section : undefined, + section_type: isCaptureText ? captureTextStep?.section_type : captureTextLineStep?.section_type, + block_id: isCaptureText ? captureTextStep?.block_id : captureTextLineStep?.block_id, + generate_block_id: isCaptureText ? captureTextStep?.generate_block_id : captureTextLineStep?.generate_block_id, + references: isCaptureText ? captureTextStep?.references : captureTextLineStep?.references, + heading_level: !isCaptureText ? captureTextLineStep?.heading_level : undefined, + }); + } console.log(`[WP-26] trackSectionInfo wird aufgerufen für Step ${step.key}, type: ${step.type}`); this.trackSectionInfo(step); } else { @@ -433,7 +449,7 @@ export class InterviewWizardModal extends Modal { let hadFocus = false; let selectionStart = 0; let selectionEnd = 0; - if (document.activeElement === textareaRef) { + if (textareaRef && document.activeElement === textareaRef) { hadFocus = true; selectionStart = textareaRef.selectionStart; selectionEnd = textareaRef.selectionEnd; @@ -1549,7 +1565,7 @@ export class InterviewWizardModal extends Modal { text.onChange((value) => { // WP-26: Speichere Fokus-Info vor State-Update - if (document.activeElement === textareaRef) { + if (textareaRef && document.activeElement === textareaRef) { hadFocus = true; selectionStart = textareaRef.selectionStart; selectionEnd = textareaRef.selectionEnd; @@ -2532,9 +2548,73 @@ export class InterviewWizardModal extends Modal { if (currentStep) { this.saveCurrentStepData(currentStep); } - this.applyPatches(); - // Run semantic mapping builder if edging mode is post_run or both + // WP-26: Zeige Übersichts-Modal für Sektions-Edges (nur wenn Sections vorhanden) + let sectionEdgeTypes: Map> | undefined = undefined; + if (this.state.sectionSequence.length > 1 && this.vocabulary) { + try { + const graphSchema = this.plugin?.ensureGraphSchemaLoaded + ? await this.plugin.ensureGraphSchemaLoaded() + : null; + + const overviewModal = new SectionEdgesOverviewModal( + this.app, + this.state.sectionSequence, + this.vocabulary, + graphSchema, + this.state.collectedData // Übergebe gesammelte Daten für Note-Link-Extraktion + ); + + const result = await overviewModal.show(); + + if (!result.cancelled && (result.sectionEdges.size > 0 || result.noteEdges.size > 0)) { + sectionEdgeTypes = result.sectionEdges; + console.log("[WP-26] Edge-Types vom Nutzer geändert:", { + sectionEdgesCount: result.sectionEdges.size, + noteEdgesCount: result.noteEdges.size, + sectionEdges: Array.from(result.sectionEdges.entries()).map(([from, toMap]) => ({ + from, + to: Array.from(toMap.entries()), + })), + noteEdges: Array.from(result.noteEdges.entries()).map(([from, toMap]) => ({ + from, + to: Array.from(toMap.entries()), + })), + }); + + // WP-26: Speichere Note-Edges für späteres Post-Run-Edging + // Diese werden in pendingEdgeAssignments gespeichert + for (const [fromBlockId, toMap] of result.noteEdges.entries()) { + for (const [toNote, edgeType] of toMap.entries()) { + // Finde Section-Key für fromBlockId + const sectionInfo = fromBlockId === "ROOT" + ? null + : this.state.sectionSequence.find(s => s.blockId === fromBlockId); + const sectionKey = sectionInfo + ? `H${sectionInfo.heading.match(/^#+/)?.length || 2}:${sectionInfo.heading.replace(/^#+\s+/, "")}` + : "ROOT"; + + this.state.pendingEdgeAssignments.push({ + filePath: this.file.path, + sectionKey: sectionKey, + linkBasename: toNote, + chosenRawType: edgeType, + createdAt: Date.now(), + }); + } + } + } + } catch (e) { + console.warn("[WP-26] Fehler beim Anzeigen des Section-Edges-Übersichts-Modals:", e); + // Continue without section edge types + } + } + + await this.applyPatches(sectionEdgeTypes); + + // WP-26: Post-Run-Edging nur noch für Note-Edges (nicht für Section-Edges) + // Section-Edges werden jetzt vollständig über die Übersichtsseite verwaltet + // Note-Edges werden weiterhin über post_run edging verarbeitet const edgingMode = this.profile.edging?.mode; console.log("[Mindnet] Checking edging mode:", { profileKey: this.profile.key, @@ -2544,12 +2624,14 @@ export class InterviewWizardModal extends Modal { }); // Support: post_run, both (inline_micro + post_run) - const shouldRunPostRun = edgingMode === "post_run" || edgingMode === "both"; + // Nur ausführen, wenn Note-Edges vorhanden sind (nicht für Section-Edges) + const shouldRunPostRun = (edgingMode === "post_run" || edgingMode === "both") && + this.state.pendingEdgeAssignments.length > 0; if (shouldRunPostRun) { - console.log("[Mindnet] Starting post-run edging"); + console.log("[Mindnet] Starting post-run edging für Note-Edges"); await this.runPostRunEdging(); } else { - console.log("[Mindnet] Post-run edging skipped (mode:", edgingMode || "none", ")"); + console.log("[Mindnet] Post-run edging skipped (mode:", edgingMode || "none", ", pending:", this.state.pendingEdgeAssignments.length, ")"); } this.onSubmit({ applied: true, patches: this.state.patches }); @@ -2565,9 +2647,9 @@ export class InterviewWizardModal extends Modal { }); }) .addButton((button) => { - button.setButtonText("Save & Exit").onClick(() => { + button.setButtonText("Save & Exit").onClick(async () => { console.log("=== SAVE & EXIT ==="); - this.applyPatches(); + await this.applyPatches(); this.onSaveAndExit({ applied: true, patches: this.state.patches, @@ -2690,8 +2772,15 @@ export class InterviewWizardModal extends Modal { if (currentBlockId) { const sourceSection = this.state.generatedBlockIds.get(currentBlockId); if (sourceSection) { - sourceType = sourceSection.sectionType || sourceSection.noteType; - console.log(`[WP-26] Source-Type aus Section: ${sourceType}`); + // WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik) + const sectionIndex = this.state.sectionSequence.findIndex(s => s.blockId === currentBlockId); + if (sectionIndex >= 0) { + sourceType = this.getEffectiveSectionType(sourceSection, sectionIndex); + console.log(`[WP-26] Source-Type aus Section (effektiv): ${sourceType}`); + } else { + sourceType = sourceSection.sectionType || sourceSection.noteType; + console.log(`[WP-26] Source-Type aus Section: ${sourceType}`); + } } } else { // Fallback: Verwende Note-Type @@ -2716,16 +2805,75 @@ export class InterviewWizardModal extends Modal { targetNoteId = sourceNoteId; // Gleiche Note } else { // Inter-Note-Edge: Normale Wikilink-Referenz - // Get source note ID and type - const sourceContent = this.fileContent; - const { extractFrontmatterId } = await import("../parser/parseFrontmatter"); - sourceNoteId = extractFrontmatterId(sourceContent) || undefined; - const sourceFrontmatter = sourceContent.match(/^---\n([\s\S]*?)\n---/); - if (sourceFrontmatter && sourceFrontmatter[1]) { - const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m); - if (typeMatch && typeMatch[1]) { - sourceType = typeMatch[1].trim(); + // WP-26: Verwende Section-Type statt Note-Type + const currentStep = step as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep; + let currentBlockId: string | null = null; + if (currentStep.block_id) { + currentBlockId = currentStep.block_id; + } else if (currentStep.generate_block_id) { + currentBlockId = slugify(currentStep.key); + } + + // Versuche Section-Type zu ermitteln + if (currentBlockId) { + const sourceSection = this.state.generatedBlockIds.get(currentBlockId); + if (sourceSection) { + // WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik) + const sectionIndex = this.state.sectionSequence.findIndex(s => s.blockId === currentBlockId); + if (sectionIndex >= 0) { + sourceType = this.getEffectiveSectionType(sourceSection, sectionIndex); + console.log(`[WP-26] Source-Type aus Section (effektiv): ${sourceType}`); + } else { + sourceType = sourceSection.sectionType || sourceSection.noteType; + console.log(`[WP-26] Source-Type aus Section: ${sourceType}`); + } + } else { + // Versuche Section-Type aus sectionSequence zu finden (auch wenn keine Block-ID vorhanden) + const sectionInfo = this.state.sectionSequence.find(s => s.stepKey === step.key); + if (sectionInfo) { + const sectionIndex = this.state.sectionSequence.findIndex(s => s.stepKey === step.key); + if (sectionIndex >= 0) { + sourceType = this.getEffectiveSectionType(sectionInfo, sectionIndex); + console.log(`[WP-26] Source-Type aus sectionSequence (effektiv): ${sourceType}`); + } else { + sourceType = sectionInfo.sectionType || sectionInfo.noteType; + console.log(`[WP-26] Source-Type aus sectionSequence: ${sourceType}`); + } + } } + } else { + // Versuche Section-Type aus sectionSequence zu finden (auch wenn keine Block-ID vorhanden) + const sectionInfo = this.state.sectionSequence.find(s => s.stepKey === step.key); + if (sectionInfo) { + const sectionIndex = this.state.sectionSequence.findIndex(s => s.stepKey === step.key); + if (sectionIndex >= 0) { + sourceType = this.getEffectiveSectionType(sectionInfo, sectionIndex); + console.log(`[WP-26] Source-Type aus sectionSequence (effektiv, kein Block-ID): ${sourceType}`); + } else { + sourceType = sectionInfo.sectionType || sectionInfo.noteType; + console.log(`[WP-26] Source-Type aus sectionSequence (kein Block-ID): ${sourceType}`); + } + } + } + + // Fallback: Verwende Note-Type aus Frontmatter + if (!sourceType) { + const sourceContent = this.fileContent; + const { extractFrontmatterId } = await import("../parser/parseFrontmatter"); + sourceNoteId = extractFrontmatterId(sourceContent) || undefined; + const sourceFrontmatter = sourceContent.match(/^---\n([\s\S]*?)\n---/); + if (sourceFrontmatter && sourceFrontmatter[1]) { + const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m); + if (typeMatch && typeMatch[1]) { + sourceType = typeMatch[1].trim(); + console.log(`[WP-26] Source-Type aus Frontmatter (Fallback): ${sourceType}`); + } + } + } else { + // sourceNoteId bereits setzen, auch wenn Section-Type gefunden wurde + const sourceContent = this.fileContent; + const { extractFrontmatterId } = await import("../parser/parseFrontmatter"); + sourceNoteId = extractFrontmatterId(sourceContent) || undefined; } // Get target note ID and type @@ -3052,7 +3200,7 @@ export class InterviewWizardModal extends Modal { }; } - async applyPatches(): Promise { + async applyPatches(sectionEdgeTypes?: Map>): Promise { console.log("=== APPLY PATCHES ===", { patchCount: this.state.patches.length, patches: this.state.patches.map(p => ({ @@ -3130,8 +3278,20 @@ export class InterviewWizardModal extends Modal { graphSchema: graphSchema, vocabulary: vocabulary, noteType: this.state.profile.note_type, + sectionEdgeTypes: sectionEdgeTypes, // WP-26: Manuell geänderte Edge-Types vom Übersichts-Modal }; + // WP-26: Debug-Log für Section-Sequenz + console.log(`[WP-26] Section-Sequenz vor Rendering:`, { + count: this.state.sectionSequence.length, + sections: this.state.sectionSequence.map(s => ({ + stepKey: s.stepKey, + blockId: s.blockId, + sectionType: s.sectionType, + heading: s.heading, + })), + }); + const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers, renderOptions); if (renderedMarkdown.trim()) { @@ -3142,6 +3302,9 @@ export class InterviewWizardModal extends Modal { contentLength: renderedMarkdown.length, preview: renderedMarkdown.substring(0, 200) + "...", }); + + // WP-26: Debug-Log für generiertes Markdown + console.log(`[WP-26] Vollständiges generiertes Markdown:`, renderedMarkdown); } // Write updated content @@ -3224,25 +3387,36 @@ export class InterviewWizardModal extends Modal { return; } - const captureStep = nestedStep as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep; + // Type-Guards für sichere Zugriffe + const isCaptureText = nestedStep.type === "capture_text"; + const isCaptureTextLine = nestedStep.type === "capture_text_line"; + + const captureTextStep = isCaptureText ? nestedStep as import("../interview/types").CaptureTextStep : null; + const captureTextLineStep = isCaptureTextLine ? nestedStep as import("../interview/types").CaptureTextLineStep : null; // Für capture_text: Nur wenn Section vorhanden ist - if (nestedStep.type === "capture_text" && !captureStep.section) { + if (isCaptureText && !captureTextStep?.section) { return; } // Für capture_text_line: Nur wenn heading_level enabled ist - if (nestedStep.type === "capture_text_line" && !captureStep.heading_level?.enabled) { + if (isCaptureTextLine && !captureTextLineStep?.heading_level?.enabled) { return; } + + // Verwende die richtige Step-Variable für die weitere Verarbeitung + const captureStep = isCaptureText ? captureTextStep! : captureTextLineStep!; // WP-26: Block-ID ermitteln + // Für Loop-Items wird die Block-ID im Renderer mit Item-Index nummeriert + // Hier tracken wir nur die Basis-Block-ID ohne Index let blockId: string | null = null; if (captureStep.block_id) { blockId = captureStep.block_id; } else if (captureStep.generate_block_id) { - // Für Loop-Items: Verwende Loop-Key + Step-Key für eindeutige Block-ID - blockId = slugify(`${loopKey}-${captureStep.key}`); + // Für Loop-Items: Verwende nur Step-Key (Index wird im Renderer hinzugefügt) + // Die tatsächliche Block-ID wird im Renderer mit Item-Index generiert + blockId = slugify(captureStep.key); } // WP-26: Section-Type ermitteln @@ -3251,10 +3425,10 @@ export class InterviewWizardModal extends Modal { // WP-26: Heading-Text ermitteln let heading = ""; - if (nestedStep.type === "capture_text" && captureStep.section) { + if (isCaptureText && captureTextStep?.section) { // Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext") - heading = captureStep.section.replace(/^#+\s+/, ""); - } else if (nestedStep.type === "capture_text_line") { + heading = captureTextStep.section.replace(/^#+\s+/, ""); + } else if (isCaptureTextLine) { // Für capture_text_line: Heading aus Draft-Wert const draftValue = draft[captureStep.key]; if (draftValue && typeof draftValue === "string") { @@ -3306,31 +3480,39 @@ export class InterviewWizardModal extends Modal { return; } - const captureStep = step as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep; + // Type-Guards für sichere Zugriffe + const isCaptureText = step.type === "capture_text"; + const isCaptureTextLine = step.type === "capture_text_line"; + + const captureTextStep = isCaptureText ? step as import("../interview/types").CaptureTextStep : null; + const captureTextLineStep = isCaptureTextLine ? step as import("../interview/types").CaptureTextLineStep : null; console.log(`[WP-26] trackSectionInfo aufgerufen für Step ${step.key}`, { type: step.type, - hasSection: !!captureStep.section, - section: captureStep.section, - hasSectionType: !!captureStep.section_type, - sectionType: captureStep.section_type, - hasBlockId: !!captureStep.block_id, - blockId: captureStep.block_id, - generateBlockId: captureStep.generate_block_id, - hasHeadingLevel: !!captureStep.heading_level?.enabled, + hasSection: isCaptureText ? !!captureTextStep?.section : false, + section: isCaptureText ? captureTextStep?.section : undefined, + hasSectionType: isCaptureText ? !!captureTextStep?.section_type : !!captureTextLineStep?.section_type, + sectionType: isCaptureText ? captureTextStep?.section_type : captureTextLineStep?.section_type, + hasBlockId: isCaptureText ? !!captureTextStep?.block_id : !!captureTextLineStep?.block_id, + blockId: isCaptureText ? captureTextStep?.block_id : captureTextLineStep?.block_id, + generateBlockId: isCaptureText ? captureTextStep?.generate_block_id : captureTextLineStep?.generate_block_id, + hasHeadingLevel: isCaptureTextLine ? !!captureTextLineStep?.heading_level?.enabled : false, }); - // Nur wenn Section vorhanden ist - if (!captureStep.section && step.type === "capture_text") { + // Nur wenn Section vorhanden ist (für capture_text) + if (isCaptureText && !captureTextStep?.section) { console.log(`[WP-26] Step ${step.key} hat keine section, überspringe`); return; } // Für capture_text_line: Nur wenn heading_level enabled ist - if (step.type === "capture_text_line" && !captureStep.heading_level?.enabled) { + if (isCaptureTextLine && !captureTextLineStep?.heading_level?.enabled) { console.log(`[WP-26] Step ${step.key} hat kein heading_level enabled, überspringe`); return; } + + // Verwende die richtige Step-Variable für die weitere Verarbeitung + const captureStep = isCaptureText ? captureTextStep! : captureTextLineStep!; // WP-26: Block-ID ermitteln let blockId: string | null = null; @@ -3346,13 +3528,13 @@ export class InterviewWizardModal extends Modal { // WP-26: Heading-Text ermitteln let heading = ""; - if (step.type === "capture_text" && captureStep.section) { + if (isCaptureText && captureTextStep?.section) { // Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext") - heading = captureStep.section.replace(/^#+\s+/, ""); - } else if (step.type === "capture_text_line") { + heading = captureTextStep.section.replace(/^#+\s+/, ""); + } else if (isCaptureTextLine) { // Für capture_text_line: Heading wird später aus dem eingegebenen Text generiert // Verwende Step-Key als Platzhalter - heading = captureStep.key; + heading = step.key; } // WP-26: Section-Info erstellen @@ -3387,6 +3569,47 @@ export class InterviewWizardModal extends Modal { } } + /** + * Ermittelt den effektiven Section-Type basierend auf Heading-Level. + * Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section + * auf dem gleichen oder höheren Level verwendet, sonst Note-Type. + */ + private getEffectiveSectionType( + section: SectionInfo, + index: number + ): string { + // Wenn expliziter Section-Type vorhanden, verwende diesen + if (section.sectionType) { + return section.sectionType; + } + + // Extrahiere Heading-Level aus der Überschrift + const headingMatch = section.heading.match(/^(#{1,6})\s+/); + const currentLevel = headingMatch ? headingMatch[1]?.length || 0 : 0; + + // Suche rückwärts nach der letzten Section mit explizitem Type auf gleichem oder höherem Level + for (let i = index - 1; i >= 0; i--) { + const prevSection = this.state.sectionSequence[i]; + if (!prevSection) continue; + + const prevHeadingMatch = prevSection.heading.match(/^(#{1,6})\s+/); + const prevLevel = prevHeadingMatch ? prevHeadingMatch[1]?.length || 0 : 0; + + // Wenn vorherige Section auf gleichem oder höherem Level einen expliziten Type hat + if (prevLevel <= currentLevel && prevSection.sectionType) { + return prevSection.sectionType; + } + + // Wenn wir auf ein höheres Level stoßen, stoppe die Suche + if (prevLevel < currentLevel) { + break; + } + } + + // Fallback: Note-Type + return section.noteType; + } + onClose(): void { const { contentEl } = this; contentEl.empty(); diff --git a/src/ui/LinkPromptModal.ts b/src/ui/LinkPromptModal.ts index e243b70..aed1d4d 100644 --- a/src/ui/LinkPromptModal.ts +++ b/src/ui/LinkPromptModal.ts @@ -67,8 +67,29 @@ export class LinkPromptModal extends Modal { const linkDisplayText = this.item.displayLink || this.item.link; linkInfo.createEl("h2", { text: `Link: [[${linkDisplayText}]]` }); - if (this.item.targetType) { - linkInfo.createEl("p", { text: `Target type: ${this.item.targetType}` }); + // WP-26: Zeige Quell- und Ziel-Typ für Übersichtlichkeit + if (this.sourceType || this.item.targetType) { + const typeInfo = linkInfo.createEl("div", { cls: "edge-type-info" }); + typeInfo.style.marginTop = "0.5em"; + typeInfo.style.padding = "0.75em"; + typeInfo.style.backgroundColor = "var(--background-modifier-hover)"; + typeInfo.style.borderRadius = "4px"; + + const sourceLabel = typeInfo.createEl("span", { text: "Quell-Typ: " }); + sourceLabel.style.fontWeight = "bold"; + const sourceValue = typeInfo.createEl("span", { + text: this.sourceType || "(unbekannt)" + }); + sourceValue.style.color = "var(--interactive-accent)"; + + typeInfo.createEl("span", { text: " → " }); + + const targetLabel = typeInfo.createEl("span", { text: "Ziel-Typ: " }); + targetLabel.style.fontWeight = "bold"; + const targetValue = typeInfo.createEl("span", { + text: this.item.targetType || "(unbekannt)" + }); + targetValue.style.color = "var(--interactive-accent)"; } // Show current/selected edge type prominently diff --git a/src/ui/SectionEdgesOverviewModal.ts b/src/ui/SectionEdgesOverviewModal.ts new file mode 100644 index 0000000..dba5c76 --- /dev/null +++ b/src/ui/SectionEdgesOverviewModal.ts @@ -0,0 +1,711 @@ +/** + * Modal für Übersicht und Bearbeitung aller Sektions-Edges am Ende des Interviews. + * WP-26: Zeigt alle Forward-Edges zwischen Sections an und ermöglicht deren Bearbeitung. + */ + +import { Modal, Notice } from "obsidian"; +import type { SectionInfo } from "../interview/wizardState"; +import type { EdgeVocabulary } from "../vocab/types"; +import type { GraphSchema } from "../mapping/graphSchema"; +import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal"; +import { getHints } from "../mapping/graphSchema"; + +export interface SectionEdge { + fromSection: SectionInfo; + toSection: SectionInfo; + edgeType: string; // Aktueller Edge-Type (kann geändert werden) + suggestedType: string | null; // Vorgeschlagener Edge-Type aus graph_schema + deleted?: boolean; // Flag für gelöschte Edges +} + +export interface NoteEdge { + fromSection: SectionInfo | null; // null = Note-Level + toNote: string; // Note-Basename + edgeType: string; // Aktueller Edge-Type (kann geändert werden) + suggestedType: string | null; // Vorgeschlagener Edge-Type aus graph_schema + targetType: string | null; // Target-Note-Type (aus Frontmatter) +} + +export interface SectionEdgesOverviewResult { + sectionEdges: Map>; // fromBlockId -> (toBlockId -> edgeType) + noteEdges: Map>; // fromBlockId (oder "ROOT") -> (toNoteBasename -> edgeType) + cancelled: boolean; +} + +export class SectionEdgesOverviewModal extends Modal { + private sectionSequence: SectionInfo[]; + private vocabulary: EdgeVocabulary | null; + private graphSchema: GraphSchema | null; + private collectedData: Map; // Gesammelte Daten aus dem Interview + private sectionEdges: SectionEdge[] = []; + private noteEdges: NoteEdge[] = []; + private result: SectionEdgesOverviewResult | null = null; + private resolve: ((result: SectionEdgesOverviewResult) => void) | null = null; + + constructor( + app: any, + sectionSequence: SectionInfo[], + vocabulary: EdgeVocabulary | null, + graphSchema: GraphSchema | null, + collectedData?: Map + ) { + super(app); + this.sectionSequence = sectionSequence; + this.vocabulary = vocabulary; + this.graphSchema = graphSchema; + this.collectedData = collectedData || new Map(); + } + + async onOpen(): Promise { + // Erstelle Edge-Liste: Für jede Section alle Forward-Edges zu vorherigen Sections + this.buildSectionEdgeList(); + // Extrahiere Note-Links aus gesammelten Daten (async) + await this.buildNoteEdgeList(); + + // Jetzt UI rendern + this.renderContent(); + } + + private renderContent(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass("section-edges-overview-modal"); + + // Header + const header = contentEl.createEl("div", { cls: "modal-header" }); + header.createEl("h2", { text: "Verbindungen bearbeiten" }); + header.createEl("p", { + text: "Überprüfen und ändern Sie die Verbindungen zwischen Sections und zu anderen Notes. Rückwärts-Verbindungen werden automatisch generiert.", + cls: "modal-description" + }); + + // Edge-Liste + const edgesContainer = contentEl.createEl("div", { cls: "edges-container" }); + + const totalEdges = this.sectionEdges.length + this.noteEdges.length; + + if (totalEdges === 0) { + edgesContainer.createEl("p", { + text: "Keine Verbindungen gefunden.", + cls: "no-edges" + }); + } else { + // 1. Section-Edges (gruppiert nach Ziel-Section) + if (this.sectionEdges.length > 0) { + const sectionEdgesHeader = edgesContainer.createEl("div", { cls: "section-edges-header" }); + sectionEdgesHeader.createEl("h3", { + text: "Sektions-Verbindungen (innerhalb der Note)", + cls: "section-edges-title" + }); + + const edgesByTarget = new Map(); + for (const edge of this.sectionEdges) { + const targetKey = edge.toSection.blockId || ""; + if (!edgesByTarget.has(targetKey)) { + edgesByTarget.set(targetKey, []); + } + edgesByTarget.get(targetKey)!.push(edge); + } + + // Zeige Edges gruppiert nach Ziel-Section + for (const [targetBlockId, targetEdges] of edgesByTarget.entries()) { + const firstEdge = targetEdges[0]; + if (!firstEdge) continue; + const targetSection = firstEdge.toSection; + if (!targetSection) continue; + + const sectionGroup = edgesContainer.createEl("div", { cls: "section-group" }); + + // Section-Header + const sectionHeader = sectionGroup.createEl("div", { cls: "section-header" }); + sectionHeader.createEl("h4", { + text: targetSection.heading || `Section ${targetBlockId}`, + cls: "section-title" + }); + if (targetSection.sectionType) { + sectionHeader.createEl("span", { + text: ` (${targetSection.sectionType})`, + cls: "section-type" + }); + } + + // Edge-Liste für diese Section + const edgeList = sectionGroup.createEl("div", { cls: "edge-list" }); + + for (const edge of targetEdges) { + const edgeItem = edgeList.createEl("div", { cls: "edge-item" }); + + // Von-Section Info + const fromInfo = edgeItem.createEl("div", { cls: "edge-from" }); + fromInfo.createEl("span", { + text: edge.fromSection.heading || `Section ${edge.fromSection.blockId}`, + cls: "edge-from-title" + }); + if (edge.fromSection.sectionType) { + fromInfo.createEl("span", { + text: ` (${edge.fromSection.sectionType})`, + cls: "edge-from-type" + }); + } + fromInfo.createEl("span", { text: " → ", cls: "edge-arrow" }); + + // WP-26: Zeige Quell- und Ziel-Typ für Übersichtlichkeit + const typeInfo = edgeItem.createEl("div", { cls: "edge-type-info" }); + typeInfo.style.fontSize = "0.85em"; + typeInfo.style.color = "var(--text-muted)"; + typeInfo.style.marginBottom = "0.25em"; + + const prevType = this.getEffectiveSectionType(edge.fromSection, this.sectionSequence.findIndex(s => s.blockId === edge.fromSection.blockId)); + const currentType = this.getEffectiveSectionType(edge.toSection, this.sectionSequence.findIndex(s => s.blockId === edge.toSection.blockId)); + + typeInfo.createEl("span", { text: `Quell: ${prevType} → Ziel: ${currentType}` }); + + // Edge-Type Display und Button + const edgeTypeContainer = edgeItem.createEl("div", { cls: "edge-type-container" }); + const currentTypeDisplay = edgeItem.createEl("div", { cls: "current-edge-type" }); + currentTypeDisplay.createEl("span", { text: "Beziehung: ", cls: "edge-label" }); + const typeValue = currentTypeDisplay.createEl("span", { + text: edge.edgeType, + cls: "edge-type-value" + }); + typeValue.style.fontWeight = "bold"; + typeValue.style.color = "var(--interactive-accent)"; + + // Button-Container + const buttonContainer = edgeItem.createEl("div", { cls: "edge-buttons" }); + buttonContainer.style.display = "flex"; + buttonContainer.style.gap = "0.5em"; + buttonContainer.style.marginTop = "0.5em"; + + // Change Button + const changeButton = buttonContainer.createEl("button", { + text: "Ändern", + cls: "mod-cta" + }); + changeButton.onclick = async () => { + await this.changeSectionEdgeType(edge, typeValue, changeButton); + }; + + // Delete Button + const deleteButton = buttonContainer.createEl("button", { + text: "Löschen", + cls: "mod-secondary" + }); + deleteButton.onclick = () => { + this.deleteSectionEdge(edge, edgeItem); + }; + } + } + } + + // 2. Note-Edges (gruppiert nach Quell-Section) + if (this.noteEdges.length > 0) { + const noteEdgesHeader = edgesContainer.createEl("div", { cls: "note-edges-header" }); + noteEdgesHeader.style.marginTop = "2em"; + noteEdgesHeader.createEl("h3", { + text: "Note-Verbindungen (zu anderen Notes)", + cls: "note-edges-title" + }); + + const edgesBySource = new Map(); + for (const edge of this.noteEdges) { + const sourceKey = edge.fromSection?.blockId || "ROOT"; + if (!edgesBySource.has(sourceKey)) { + edgesBySource.set(sourceKey, []); + } + edgesBySource.get(sourceKey)!.push(edge); + } + + // Zeige Edges gruppiert nach Quell-Section + for (const [sourceKey, sourceEdges] of edgesBySource.entries()) { + const sectionGroup = edgesContainer.createEl("div", { cls: "section-group" }); + + // Section-Header + const sectionHeader = sectionGroup.createEl("div", { cls: "section-header" }); + if (sourceKey === "ROOT") { + sectionHeader.createEl("h4", { + text: "Note-Level", + cls: "section-title" + }); + } else { + const sourceSection = sourceEdges[0]?.fromSection; + if (sourceSection) { + sectionHeader.createEl("h4", { + text: sourceSection.heading || `Section ${sourceKey}`, + cls: "section-title" + }); + if (sourceSection.sectionType) { + sectionHeader.createEl("span", { + text: ` (${sourceSection.sectionType})`, + cls: "section-type" + }); + } + } + } + + // Edge-Liste für diese Section + const edgeList = sectionGroup.createEl("div", { cls: "edge-list" }); + + for (const edge of sourceEdges) { + const edgeItem = edgeList.createEl("div", { cls: "edge-item" }); + + // WP-26: Zeige Quell-Section Info (falls vorhanden) + if (edge.fromSection) { + const fromInfo = edgeItem.createEl("div", { cls: "edge-from" }); + fromInfo.style.marginBottom = "0.5em"; + fromInfo.createEl("span", { + text: "Von: ", + cls: "edge-label" + }).style.fontWeight = "bold"; + fromInfo.createEl("span", { + text: edge.fromSection.heading || `Section ${edge.fromSection.blockId}`, + cls: "edge-from-title" + }); + if (edge.fromSection.sectionType) { + fromInfo.createEl("span", { + text: ` (${edge.fromSection.sectionType})`, + cls: "edge-from-type" + }); + } + } else { + const fromInfo = edgeItem.createEl("div", { cls: "edge-from" }); + fromInfo.style.marginBottom = "0.5em"; + fromInfo.createEl("span", { + text: "Von: Note-Level", + cls: "edge-label" + }).style.fontWeight = "bold"; + } + + // Zu-Note Info (sehr prominent) + const toInfo = edgeItem.createEl("div", { cls: "edge-to" }); + toInfo.style.marginBottom = "0.5em"; + toInfo.style.padding = "0.5em"; + toInfo.style.backgroundColor = "var(--background-modifier-hover)"; + toInfo.style.borderRadius = "4px"; + toInfo.style.borderLeft = "3px solid var(--interactive-accent)"; + + const toLabel = toInfo.createEl("span", { + text: "Ziel-Note: ", + cls: "edge-label" + }); + toLabel.style.fontWeight = "bold"; + toLabel.style.fontSize = "0.9em"; + toLabel.style.color = "var(--text-muted)"; + + const noteLink = toInfo.createEl("span", { + text: `[[${edge.toNote}]]`, + cls: "edge-to-note" + }); + noteLink.style.fontWeight = "bold"; + noteLink.style.fontSize = "1.1em"; + noteLink.style.color = "var(--interactive-accent)"; + noteLink.style.marginLeft = "0.5em"; + + if (edge.targetType) { + const typeInfo = toInfo.createEl("span", { + text: ` (Typ: ${edge.targetType})`, + cls: "edge-to-type" + }); + typeInfo.style.color = "var(--text-muted)"; + typeInfo.style.fontSize = "0.9em"; + typeInfo.style.marginLeft = "0.5em"; + } + + // WP-26: Zeige Quell- und Ziel-Typ für Übersichtlichkeit + const typeInfo = edgeItem.createEl("div", { cls: "edge-type-info" }); + typeInfo.style.fontSize = "0.85em"; + typeInfo.style.color = "var(--text-muted)"; + typeInfo.style.marginBottom = "0.25em"; + + const sourceType = edge.fromSection + ? this.getEffectiveSectionType(edge.fromSection, this.sectionSequence.findIndex(s => s.blockId === edge.fromSection?.blockId) || 0) + : "(Note-Level)"; + const targetType = edge.targetType || "(unbekannt)"; + + typeInfo.createEl("span", { text: `Quell-Typ: ${sourceType} → Ziel-Typ: ${targetType}` }); + + // Edge-Type Display und Button + const edgeTypeContainer = edgeItem.createEl("div", { cls: "edge-type-container" }); + const currentTypeDisplay = edgeItem.createEl("div", { cls: "current-edge-type" }); + currentTypeDisplay.createEl("span", { text: "Beziehung: ", cls: "edge-label" }); + const typeValue = currentTypeDisplay.createEl("span", { + text: edge.edgeType, + cls: "edge-type-value" + }); + typeValue.style.fontWeight = "bold"; + typeValue.style.color = "var(--interactive-accent)"; + + // Button-Container + const buttonContainer = edgeItem.createEl("div", { cls: "edge-buttons" }); + buttonContainer.style.display = "flex"; + buttonContainer.style.gap = "0.5em"; + buttonContainer.style.marginTop = "0.5em"; + + // Change Button + const changeButton = buttonContainer.createEl("button", { + text: "Ändern", + cls: "mod-cta" + }); + changeButton.onclick = async () => { + await this.changeNoteEdgeType(edge, typeValue, changeButton); + }; + } + } + } + } + + // Buttons + const buttonContainer = contentEl.createEl("div", { cls: "modal-button-container" }); + + buttonContainer.createEl("button", { text: "Abbrechen", cls: "mod-secondary" }) + .onclick = () => { + this.result = { sectionEdges: new Map(), noteEdges: new Map(), cancelled: true }; + this.close(); + }; + + buttonContainer.createEl("button", { text: "Übernehmen", cls: "mod-cta" }) + .onclick = () => { + this.saveEdges(); + this.close(); + }; + } + + /** + * Ermittelt den effektiven Section-Type basierend auf Heading-Level. + * Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section + * auf dem gleichen oder höheren Level verwendet, sonst Note-Type. + */ + private getEffectiveSectionType(section: SectionInfo, index: number): string { + // Wenn expliziter Section-Type vorhanden, verwende diesen + if (section.sectionType) { + return section.sectionType; + } + + // Extrahiere Heading-Level aus der Überschrift + const headingMatch = section.heading.match(/^(#{1,6})\s+/); + const currentLevel = headingMatch ? headingMatch[1]?.length || 0 : 0; + + // Suche rückwärts nach der letzten Section mit explizitem Type auf gleichem oder höherem Level + for (let i = index - 1; i >= 0; i--) { + const prevSection = this.sectionSequence[i]; + if (!prevSection) continue; + + const prevHeadingMatch = prevSection.heading.match(/^(#{1,6})\s+/); + const prevLevel = prevHeadingMatch ? prevHeadingMatch[1]?.length || 0 : 0; + + // Wenn vorherige Section auf gleichem oder höherem Level einen expliziten Type hat + if (prevLevel <= currentLevel && prevSection.sectionType) { + return prevSection.sectionType; + } + + // Wenn wir auf ein höheres Level stoßen, stoppe die Suche + if (prevLevel < currentLevel) { + break; + } + } + + // Fallback: Note-Type + return section.noteType; + } + + private buildSectionEdgeList(): void { + this.sectionEdges = []; + + for (let i = 1; i < this.sectionSequence.length; i++) { + const currentSection = this.sectionSequence[i]; + if (!currentSection || !currentSection.blockId) continue; + + // Alle vorherigen Sections + for (let j = 0; j < i; j++) { + const prevSection = this.sectionSequence[j]; + if (!prevSection || !prevSection.blockId) continue; + + // WP-26: Verwende effektive Section-Types (mit Heading-Level-basierter Fallback-Logik) + const prevType = this.getEffectiveSectionType(prevSection, j); + const currentType = this.getEffectiveSectionType(currentSection, i); + + let suggestedType: string | null = null; + if (this.graphSchema && prevType && currentType) { + const hints = getHints(this.graphSchema, prevType, currentType); + if (hints && hints.typical && hints.typical.length > 0) { + suggestedType = hints.typical[0] || null; + } + } + + // Fallback: related_to für experience -> experience, references für andere + if (!suggestedType) { + if (prevType === currentType && prevType === "experience") { + suggestedType = "related_to"; + } else { + suggestedType = "references"; + } + } + + this.sectionEdges.push({ + fromSection: prevSection, + toSection: currentSection, + edgeType: suggestedType, // Initial: vorgeschlagener Type + suggestedType: suggestedType, + }); + } + } + } + + private async buildNoteEdgeList(): Promise { + this.noteEdges = []; + + // Extrahiere alle Note-Links aus gesammelten Daten + const noteLinksBySection = new Map>(); // blockId -> Set + + for (const [key, value] of this.collectedData.entries()) { + if (!value || typeof value !== "string") continue; + + // Finde zugehörige Section für diesen Step + let sectionInfo: SectionInfo | null = null; + for (const section of this.sectionSequence) { + if (section.stepKey === key) { + sectionInfo = section; + break; + } + } + + // Extrahiere Wikilinks aus dem Text + const wikilinkRegex = /\[\[([^\]]+?)\]\]/g; + let match: RegExpExecArray | null; + while ((match = wikilinkRegex.exec(value)) !== null) { + if (match[1]) { + const linkTarget = match[1].trim(); + if (!linkTarget) continue; + + // Prüfe, ob es ein Block-ID-Link ist ([[#^...]]) + if (linkTarget.startsWith("#^")) { + continue; // Block-ID-Links sind Section-Edges, keine Note-Edges + } + + // Normalisiere Link (entferne Alias und Heading) + const normalized = linkTarget.split("|")[0]?.split("#")[0]?.trim() || linkTarget; + if (!normalized) continue; + + const blockId = sectionInfo?.blockId || null; + if (!noteLinksBySection.has(blockId)) { + noteLinksBySection.set(blockId, new Set()); + } + noteLinksBySection.get(blockId)!.add(normalized); + } + } + } + + // Erstelle NoteEdge-Objekte + for (const [blockId, noteLinks] of noteLinksBySection.entries()) { + const sectionInfo = blockId + ? this.sectionSequence.find(s => s.blockId === blockId) || null + : null; + + // WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik) + const sectionIndex = blockId ? this.sectionSequence.findIndex(s => s.blockId === blockId) : -1; + const sourceType = sectionInfo && sectionIndex >= 0 + ? this.getEffectiveSectionType(sectionInfo, sectionIndex) + : null; + + for (const noteBasename of noteLinks) { + // Ermittle Target-Note-Type + let targetType: string | null = null; + try { + const targetFile = this.app.metadataCache.getFirstLinkpathDest(noteBasename, ""); + if (targetFile) { + const cache = this.app.metadataCache.getFileCache(targetFile); + if (cache?.frontmatter) { + targetType = cache.frontmatter.type || cache.frontmatter.noteType || null; + if (targetType && typeof targetType !== "string") { + targetType = String(targetType); + } + } + } + } catch (e) { + // Ignore errors + } + + // Ermittle vorgeschlagenen Edge-Type aus graph_schema + let suggestedType: string | null = null; + if (this.graphSchema && sourceType && targetType) { + const hints = getHints(this.graphSchema, sourceType, targetType); + if (hints.typical.length > 0) { + suggestedType = hints.typical[0] || null; + } + } + + // Fallback: references + if (!suggestedType) { + suggestedType = "references"; + } + + this.noteEdges.push({ + fromSection: sectionInfo, + toNote: noteBasename, + edgeType: suggestedType, + suggestedType: suggestedType, + targetType: targetType, + }); + } + } + } + + private async changeSectionEdgeType( + edge: SectionEdge, + typeDisplay: HTMLElement, + button: HTMLElement + ): Promise { + if (!this.vocabulary) { + new Notice("Vocabulary nicht verfügbar"); + return; + } + + const prevType = edge.fromSection.sectionType || edge.fromSection.noteType; + const currentType = edge.toSection.sectionType || edge.toSection.noteType; + + // Erstelle EdgeTypeChooserModal (erwartet EdgeVocabulary) + const chooser = new EdgeTypeChooserModal( + this.app, + this.vocabulary, + prevType, + currentType, + this.graphSchema, + [] // Keine zusätzlichen Vorschläge + ); + + const choice = await chooser.show(); + + if (choice) { + edge.edgeType = choice.alias || choice.edgeType; + typeDisplay.textContent = edge.edgeType; + + // Visuelles Feedback + button.textContent = "✓ Geändert"; + button.style.backgroundColor = "var(--interactive-success)"; + setTimeout(() => { + button.textContent = "Ändern"; + button.style.backgroundColor = ""; + }, 2000); + } + } + + private async changeNoteEdgeType( + edge: NoteEdge, + typeDisplay: HTMLElement, + button: HTMLElement + ): Promise { + if (!this.vocabulary) { + new Notice("Vocabulary nicht verfügbar"); + return; + } + + // WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik) + const sourceType = edge.fromSection + ? this.getEffectiveSectionType(edge.fromSection, this.sectionSequence.findIndex(s => s.blockId === edge.fromSection?.blockId) || 0) + : null; + const targetType = edge.targetType; + + // Erstelle EdgeTypeChooserModal (erwartet EdgeVocabulary) + const chooser = new EdgeTypeChooserModal( + this.app, + this.vocabulary, + sourceType, + targetType, + this.graphSchema, + [] // Keine zusätzlichen Vorschläge + ); + + const choice = await chooser.show(); + + if (choice) { + edge.edgeType = choice.alias || choice.edgeType; + typeDisplay.textContent = edge.edgeType; + + // Visuelles Feedback + button.textContent = "✓ Geändert"; + button.style.backgroundColor = "var(--interactive-success)"; + setTimeout(() => { + button.textContent = "Ändern"; + button.style.backgroundColor = ""; + }, 2000); + } + } + + private deleteSectionEdge(edge: SectionEdge, edgeItem: HTMLElement): void { + // Markiere Edge als gelöscht + edge.deleted = true; + + // Entferne visuell aus der UI + edgeItem.style.opacity = "0.5"; + edgeItem.style.textDecoration = "line-through"; + + // Deaktiviere Buttons + const buttons = edgeItem.querySelectorAll("button"); + buttons.forEach(btn => { + btn.disabled = true; + }); + + console.log(`[WP-26] Section-Edge gelöscht: ${edge.fromSection.blockId} -> ${edge.toSection.blockId}`); + } + + private saveEdges(): void { + const sectionEdgesMap = new Map>(); + const noteEdgesMap = new Map>(); + + // Speichere Section-Edges (nur nicht-gelöschte) + for (const edge of this.sectionEdges) { + // Überspringe gelöschte Edges + if (edge.deleted) { + continue; + } + + const fromBlockId = edge.fromSection.blockId; + const toBlockId = edge.toSection.blockId; + + if (!fromBlockId || !toBlockId) continue; + + if (!sectionEdgesMap.has(fromBlockId)) { + sectionEdgesMap.set(fromBlockId, new Map()); + } + + sectionEdgesMap.get(fromBlockId)!.set(toBlockId, edge.edgeType); + } + + // Speichere Note-Edges + for (const edge of this.noteEdges) { + const fromBlockId = edge.fromSection?.blockId || "ROOT"; + const toNote = edge.toNote; + + if (!toNote) continue; + + if (!noteEdgesMap.has(fromBlockId)) { + noteEdgesMap.set(fromBlockId, new Map()); + } + + noteEdgesMap.get(fromBlockId)!.set(toNote, edge.edgeType); + } + + this.result = { + sectionEdges: sectionEdgesMap, + noteEdges: noteEdgesMap, + cancelled: false + }; + } + + show(): Promise { + return new Promise((resolve) => { + this.resolve = resolve; + this.open(); + }); + } + + onClose(): void { + const { contentEl } = this; + contentEl.empty(); + + if (this.resolve) { + this.resolve(this.result || { sectionEdges: new Map(), noteEdges: new Map(), cancelled: true }); + this.resolve = null; + } + } +}