Enhance interview configuration and documentation for WP-26 integration
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- Added the `Interview_Config_Guide.md` for comprehensive instructions on creating interview profiles and utilizing various note types.
- Updated `00_Dokumentations_Index.md` to include links to the new guide and improved navigation for WP-26 related resources.
- Enhanced `06_Konfigurationsdateien_Referenz.md` with references to the new guide and clarified YAML structure for interview configurations.
- Introduced `audit_geburtsdatei.md` for detailed analysis of section connections and edge types, highlighting critical issues and recommendations for improvement.
- Improved renderer tests to ensure proper handling of section types and edge generation, aligning with the new WP-26 features.
This commit is contained in:
Lars 2026-01-30 12:37:06 +01:00
parent b99416b67d
commit 8186ca5ce0
13 changed files with 2805 additions and 282 deletions

View File

@ -19,13 +19,14 @@
### Spezialisierte Referenzen ### Spezialisierte Referenzen
6. **[06_Konfigurationsdateien_Referenz.md](./06_Konfigurationsdateien_Referenz.md)** - Config-Dateien Format & Aufbau 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) ### 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. **[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_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 11. **[WP26_Implementation_Checklist.md](./WP26_Implementation_Checklist.md)** - Implementierungs-Checkliste mit Tasks und Phasen
### Chain Inspector Reports ### Chain Inspector Reports
@ -118,7 +119,7 @@
- [x] **edge_vocabulary.md** - Format, Parsing-Regeln, Beispiel - [x] **edge_vocabulary.md** - Format, Parsing-Regeln, Beispiel
- [x] **graph_schema.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_roles.yaml** - Format, Felder, Beispiel (Roles, Edge Types)
- [x] **chain_templates.yaml** - Format, Felder, Beispiel (Templates, Slots, Links, Defaults, Profiles) - [x] **chain_templates.yaml** - Format, Felder, Beispiel (Templates, Slots, Links, Defaults, Profiles)
- [x] **analysis_policies.yaml** - Geplante Struktur (noch nicht vollständig implementiert) - [x] **analysis_policies.yaml** - Geplante Struktur (noch nicht vollständig implementiert)
@ -168,6 +169,7 @@
### Konfiguration ### Konfiguration
→ [02_Administratorhandbuch.md](./02_Administratorhandbuch.md) → [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 ### Nutzung
→ [01_Benutzerhandbuch.md](./01_Benutzerhandbuch.md) → [01_Benutzerhandbuch.md](./01_Benutzerhandbuch.md)

View File

@ -175,41 +175,22 @@ Definiert Profile, Steps, Loops für Note-Erstellung und Interviews.
**YAML-Datei** mit strukturierter Hierarchie. **YAML-Datei** mit strukturierter Hierarchie.
### Struktur ### Vollständige Dokumentation
```yaml **→ [Interview_Config_Guide.md](./Interview_Config_Guide.md)** - Vollständige Anleitung mit:
version: "2.0" - Alle Step-Typen (`capture_frontmatter`, `capture_text`, `capture_text_line`, `loop`, `review`, etc.)
frontmatter_whitelist: - WP-26 Features (Section Types, Block-IDs, Referenzen)
- tags - Beispiele für verschiedene Note-Typen (experience, insight, decision, principle)
- status - Best Practices & Patterns
- GenAI-Prompt-Template
profiles: ### Kurzübersicht
- 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
#### Root-Level #### Root-Level
- **`version`** (string, optional): Config-Version (Standard: "2.0") - **`version`** (string, optional): Config-Version (aktuell: `3`)
- **`frontmatter_whitelist`** (array of strings, optional): Erlaubte Frontmatter-Keys (zusätzlich zu Standard) - **`frontmatter_whitelist`** (array of strings, optional): Erlaubte Frontmatter-Keys
- **`ui_defaults`** (object, optional): Standard-UI-Einstellungen
- **`profiles`** (array, required): Liste von Profilen - **`profiles`** (array, required): Liste von Profilen
#### Profile #### Profile
@ -217,72 +198,66 @@ profiles:
- **`key`** (string, required): Eindeutiger Profil-Schlüssel - **`key`** (string, required): Eindeutiger Profil-Schlüssel
- **`label`** (string, required): Anzeige-Name - **`label`** (string, required): Anzeige-Name
- **`note_type`** (string, required): Note-Type (z.B. `experience`, `insight`, `decision`) - **`note_type`** (string, required): Note-Type (z.B. `experience`, `insight`, `decision`)
- **`description`** (string, optional): Beschreibung - **`group`** (string, optional): Gruppierung für UI
- **`defaults`** (object, optional): Standardwerte - **`defaults`** (object, optional): Standardwerte (status, folder, chunking_profile, etc.)
- **`folder`** (string, optional): Standard-Ordner - **`edging`** (object, optional): Semantic Mapping Konfiguration (`mode`, `wrapperCalloutType`, etc.)
- **`tags`** (array of strings, optional): Standard-Tags - **`steps`** (array, required): Liste von Steps
- Weitere Felder möglich
- **`steps`** (array, optional): Interview-Steps
- **`post_run`** (object, optional): Post-Run-Actions
- **`edger`** (boolean, optional): Semantic Mapping Builder ausführen
#### Step #### Step-Typen
- **`id`** (string, required): Eindeutige Step-ID - **`capture_frontmatter`**: Frontmatter-Feld erfassen
- **`prompt`** (string, required): Prompt-Text - **`capture_text`**: Mehrzeiligen Text erfassen (mit Section-Header)
- **`input_type`** (string, required): Input-Typ (`text`, `textarea`, `select`, etc.) - **`capture_text_line`**: Einzeiligen Text erfassen (optional mit Heading-Level)
- **`required`** (boolean, optional): Ob Step erforderlich ist - **`loop`**: Wiederholbare Steps für Listen
- **`default`** (string, optional): Standardwert - **`review`**: Review & Apply Step
- **`options`** (array, optional): Optionen für `select` Input-Type - **`instruction`**: Anweisung anzeigen
- **`loop`** (object, optional): Loop-Konfiguration - **`llm_dialog`**: LLM-Dialog (experimentell)
- **`key`** (string, required): Loop-Key - **`entity_picker`**: Entity-Auswahl (experimentell)
- **`prompt`** (string, required): Loop-Prompt
- **`min_items`** (number, optional): Minimale Anzahl Items #### WP-26 Features (nur für `capture_text` und `capture_text_line`)
- **`max_items`** (number, optional): Maximale Anzahl Items
- **`nested_loops`** (array, optional): Verschachtelte Loops - **`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 ### Beispiel
```yaml ```yaml
version: "2.0" version: 3
frontmatter_whitelist:
- tags
- status
profiles: profiles:
- key: experience_basic - key: experience_basic
group: experience
label: "Erfahrung (Basis)" label: "Erfahrung (Basis)"
note_type: experience note_type: experience
description: "Basic experience profile"
defaults: defaults:
folder: "experiences" status: active
tags: ["experience"] folder: "03_experience"
edging:
mode: both
steps: steps:
- id: context - id: title
prompt: "Beschreibe den Kontext" kind: capture_frontmatter
input_type: textarea field: title
label: "Titel"
required: true 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 - id: context
label: "Einsicht (Basis)" kind: capture_text
note_type: insight section: "## 📖 Kontext"
defaults: label: "Kontext"
folder: "insights"
steps:
- id: insight
prompt: "Beschreibe die Einsicht"
input_type: textarea
required: true 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 ### Verwendung
@ -290,7 +265,21 @@ profiles:
- **Profil-Auswahl:** Profile werden in ProfileSelectionModal angezeigt - **Profil-Auswahl:** Profile werden in ProfileSelectionModal angezeigt
- **Wizard:** Steps werden als Wizard-Steps angezeigt - **Wizard:** Steps werden als Wizard-Steps angezeigt
- **Frontmatter:** Frontmatter wird mit Whitelist generiert - **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.
--- ---

View File

@ -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: <block_id_der_referenzierten_section>
edge_type: <optional_vorgeschlagener_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**

146
docs/audit_geburtsdatei.md Normal file
View File

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

View File

@ -46,8 +46,6 @@ describe("WP-26 Interview Renderer", () => {
it("sollte keine Selbstreferenz bei automatischen Edges generieren", () => { it("sollte keine Selbstreferenz bei automatischen Edges generieren", () => {
const profile: InterviewProfile = { const profile: InterviewProfile = {
version: "2.0",
frontmatterWhitelist: [],
key: "test", key: "test",
label: "Test", label: "Test",
note_type: "experience", note_type: "experience",
@ -80,7 +78,7 @@ describe("WP-26 Interview Renderer", () => {
const result = renderProfileToMarkdown(profile, answers, mockOptions); const result = renderProfileToMarkdown(profile, answers, mockOptions);
// Prüfe, dass keine Selbstreferenz vorhanden ist // 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(); expect(contextSection).toBeDefined();
// Prüfe, dass keine Edge auf sich selbst zeigt // 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", () => { it("sollte Edge-Types aus graph_schema verwenden", () => {
const profile: InterviewProfile = { const profile: InterviewProfile = {
version: "2.0",
frontmatterWhitelist: [],
key: "test", key: "test",
label: "Test", label: "Test",
note_type: "experience", note_type: "experience",
@ -150,8 +146,6 @@ describe("WP-26 Interview Renderer", () => {
it("sollte Backlinks automatisch generieren", () => { it("sollte Backlinks automatisch generieren", () => {
const profile: InterviewProfile = { const profile: InterviewProfile = {
version: "2.0",
frontmatterWhitelist: [],
key: "test", key: "test",
label: "Test", label: "Test",
note_type: "experience", note_type: "experience",
@ -193,8 +187,6 @@ describe("WP-26 Interview Renderer", () => {
it("sollte keine Selbstreferenz bei expliziten Referenzen erlauben", () => { it("sollte keine Selbstreferenz bei expliziten Referenzen erlauben", () => {
const profile: InterviewProfile = { const profile: InterviewProfile = {
version: "2.0",
frontmatterWhitelist: [],
key: "test", key: "test",
label: "Test", label: "Test",
note_type: "experience", note_type: "experience",
@ -232,8 +224,6 @@ describe("WP-26 Interview Renderer", () => {
it("sollte Section-Type-Callout direkt nach Heading platzieren", () => { it("sollte Section-Type-Callout direkt nach Heading platzieren", () => {
const profile: InterviewProfile = { const profile: InterviewProfile = {
version: "2.0",
frontmatterWhitelist: [],
key: "test", key: "test",
label: "Test", label: "Test",
note_type: "experience", note_type: "experience",
@ -258,14 +248,12 @@ describe("WP-26 Interview Renderer", () => {
const result = renderProfileToMarkdown(profile, answers, mockOptions); const result = renderProfileToMarkdown(profile, answers, mockOptions);
// Prüfe, dass Section-Type-Callout direkt nach Heading steht // 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); expect(result).toMatch(sectionPattern);
}); });
it("sollte Edges am Ende der Sektion platzieren", () => { it("sollte Edges am Ende der Sektion platzieren", () => {
const profile: InterviewProfile = { const profile: InterviewProfile = {
version: "2.0",
frontmatterWhitelist: [],
key: "test", key: "test",
label: "Test", label: "Test",
note_type: "experience", note_type: "experience",
@ -298,7 +286,7 @@ describe("WP-26 Interview Renderer", () => {
const result = renderProfileToMarkdown(profile, answers, mockOptions); const result = renderProfileToMarkdown(profile, answers, mockOptions);
// Prüfe, dass Edges am Ende der Einsicht-Sektion stehen (nach dem Text) // 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).toBeDefined();
expect(insightSection).toMatch(/> \[!edge\]/); expect(insightSection).toMatch(/> \[!edge\]/);
}); });

View File

@ -23,6 +23,7 @@ export interface RenderOptions {
graphSchema?: GraphSchema | null; graphSchema?: GraphSchema | null;
vocabulary?: Vocabulary | null; vocabulary?: Vocabulary | null;
noteType?: string; // Fallback für effective_type noteType?: string; // Fallback für effective_type
sectionEdgeTypes?: Map<string, Map<string, string>>; // fromBlockId -> (toBlockId -> edgeType) für manuell geänderte Edge-Types
} }
/** /**
@ -35,13 +36,20 @@ export function renderProfileToMarkdown(
answers: RenderAnswers, answers: RenderAnswers,
options?: RenderOptions options?: RenderOptions
): string { ): string {
const output: string[] = [];
// WP-26: Verwende übergebene Section-Sequenz oder erstelle neue // WP-26: Verwende übergebene Section-Sequenz oder erstelle neue
// Die Section-Sequenz wird während des interaktiven Wizard-Durchlaufs getrackt // Die Section-Sequenz wird während des interaktiven Wizard-Durchlaufs getrackt
const sectionSequence: SectionInfo[] = answers.sectionSequence ? [...answers.sectionSequence] : []; const sectionSequence: SectionInfo[] = answers.sectionSequence ? [...answers.sectionSequence] : [];
const generatedBlockIds = new Map<string, SectionInfo>(); const generatedBlockIds = new Map<string, SectionInfo>();
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 // WP-26: Wenn Section-Sequenz übergeben wurde, fülle generatedBlockIds aus der Sequenz
if (answers.sectionSequence && answers.sectionSequence.length > 0) { if (answers.sectionSequence && answers.sectionSequence.length > 0) {
for (const sectionInfo of answers.sectionSequence) { 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 = {
for (const step of profile.steps) {
const stepOutput = renderStep(
step,
answers,
{
profile, profile,
sectionSequence, sectionSequence,
generatedBlockIds, generatedBlockIds,
options: options || {}, 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) {
// 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) { 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); 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<string, string>(); // 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 // WP-26: Render-Kontext für Section-Types und Block-IDs
@ -126,6 +235,17 @@ function renderCaptureText(
const text = String(value); 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 // WP-26: Block-ID-Generierung
let blockId: string | null = null; let blockId: string | null = null;
if (step.block_id) { if (step.block_id) {
@ -134,6 +254,8 @@ function renderCaptureText(
blockId = slugify(step.key); blockId = slugify(step.key);
} }
console.log(`[WP-26] Generierte Block-ID für Step ${step.key}:`, blockId);
// WP-26: Heading mit Block-ID erweitern // WP-26: Heading mit Block-ID erweitern
let heading = step.section || ""; let heading = step.section || "";
if (heading && blockId) { if (heading && blockId) {
@ -158,6 +280,13 @@ function renderCaptureText(
// WP-26: Block-ID tracken // WP-26: Block-ID tracken
if (blockId) { if (blockId) {
context.generatedBlockIds.set(blockId, sectionInfo); 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) // WP-26: Section-Sequenz aktualisieren (nur wenn Section vorhanden)
@ -165,11 +294,15 @@ function renderCaptureText(
context.sectionSequence.push(sectionInfo); 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); const references = renderReferences(step.references || [], context, sectionInfo);
// WP-26: Automatische Edge-Vorschläge generieren (am Ende der Sektion) // WP-26: Automatische Forward-Edges generieren (prevSection -> currentSection)
const autoEdges = renderAutomaticEdges(sectionInfo, context); 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 // Use template if provided
if (step.output?.template) { if (step.output?.template) {
@ -180,12 +313,12 @@ function renderCaptureText(
}); });
// WP-26: Template mit Heading, Section-Type und Edges kombinieren // 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 // Default: use section if provided, otherwise just the text
if (step.section) { if (step.section) {
return combineSectionParts(heading, sectionTypeCallout, text, references, autoEdges); return combineSectionParts(heading, sectionTypeCallout, text, references, forwardEdges, backwardEdges);
} }
return text; return text;
@ -223,6 +356,7 @@ function renderCaptureTextLine(
} }
// WP-26: Block-ID-Generierung // 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; let blockId: string | null = null;
if (step.block_id) { if (step.block_id) {
blockId = step.block_id; blockId = step.block_id;
@ -245,28 +379,9 @@ function renderCaptureTextLine(
? `> [!section] ${step.section_type}` ? `> [!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) // WP-26: Section-Info für Tracking (nur wenn Heading vorhanden)
let sectionInfo: SectionInfo | null = null; let sectionInfo: SectionInfo | null = null;
if (headingPrefix) { if (headingPrefix && blockId) {
sectionInfo = { sectionInfo = {
stepKey: step.key, stepKey: step.key,
sectionType: step.section_type || null, sectionType: step.section_type || null,
@ -275,23 +390,28 @@ function renderCaptureTextLine(
noteType: context.options.noteType || context.profile.note_type, noteType: context.options.noteType || context.profile.note_type,
}; };
// WP-26: Block-ID tracken // WP-26: Block-ID tracken (auch wenn nummeriert für Loop-Items)
if (blockId) {
context.generatedBlockIds.set(blockId, sectionInfo); 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 // WP-26: Section-Sequenz aktualisieren
context.sectionSequence.push(sectionInfo); context.sectionSequence.push(sectionInfo);
} }
// WP-26: Referenzen generieren (am Ende der Sektion) // WP-26: Referenzen generieren (Forward-Edges zu anderen Sections)
const references = sectionInfo const references = sectionInfo
? renderReferences(step.references || [], context, sectionInfo) ? renderReferences(step.references || [], context, sectionInfo)
: ""; : "";
// WP-26: Automatische Edge-Vorschläge generieren (nur wenn Heading vorhanden) // WP-26: Automatische Forward-Edges generieren (prevSection -> currentSection)
const autoEdges = sectionInfo const forwardEdges = sectionInfo
? renderAutomaticEdges(sectionInfo, context) ? renderForwardEdges(sectionInfo, context)
: ""; : "";
// Use template if provided // Use template if provided
@ -305,7 +425,8 @@ function renderCaptureTextLine(
// WP-26: Template mit Heading, Section-Type und Edges kombinieren (wenn Heading vorhanden) // WP-26: Template mit Heading, Section-Type und Edges kombinieren (wenn Heading vorhanden)
if (headingPrefix) { 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; return templateText;
@ -313,7 +434,8 @@ function renderCaptureTextLine(
// WP-26: Wenn Heading vorhanden, mit Section-Type und Edges kombinieren // WP-26: Wenn Heading vorhanden, mit Section-Type und Edges kombinieren
if (headingPrefix) { 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 // Default: text with heading prefix if configured
@ -401,11 +523,15 @@ function renderLoopRecursive(
const itemOutputs: string[] = []; const itemOutputs: string[] = [];
// WP-26: Tracke Item-Index für eindeutige Block-IDs
let itemIndex = 0;
for (const item of items) { for (const item of items) {
if (!item || typeof item !== "object") { if (!item || typeof item !== "object") {
continue; continue;
} }
itemIndex++;
const itemParts: string[] = []; const itemParts: string[] = [];
// Render each nested step for this item (recursively) // Render each nested step for this item (recursively)
@ -417,8 +543,16 @@ function renderLoopRecursive(
const text = String(fieldValue); const text = String(fieldValue);
const captureStep = nestedStep as CaptureTextStep; 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 // WP-26: Verwende erweiterte renderCaptureText Funktion
const rendered = renderCaptureText(captureStep, { const rendered = renderCaptureText(loopStepWithBlockId, {
collectedData: new Map([[nestedStep.key, fieldValue]]), collectedData: new Map([[nestedStep.key, fieldValue]]),
loopContexts: answers.loopContexts, loopContexts: answers.loopContexts,
}, context); }, context);
@ -442,8 +576,16 @@ function renderLoopRecursive(
itemData.set(headingLevelKey, headingLevel); 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 // WP-26: Verwende erweiterte renderCaptureTextLine Funktion
const rendered = renderCaptureTextLine(captureStep, { const rendered = renderCaptureTextLine(loopStepWithBlockId, {
collectedData: itemData, collectedData: itemData,
loopContexts: answers.loopContexts, loopContexts: answers.loopContexts,
}, context); }, 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. * 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( function combineSectionParts(
heading: string, heading: string,
sectionTypeCallout: string, sectionTypeCallout: string,
content: string, content: string,
references: string, references: string,
autoEdges: string autoEdges: string,
backwardEdges: string = ""
): string { ): string {
const parts: string[] = []; const parts: string[] = [];
@ -560,18 +703,97 @@ function combineSectionParts(
parts.push(content); parts.push(content);
} }
// WP-26: Edges am Ende der Sektion (Referenzen + automatische Edges) // WP-26: Alle Edges im abstract wrapper am Ende der Sektion
const edges = [references, autoEdges].filter(e => e.trim()).join("\n"); const allEdges = [references, autoEdges, backwardEdges].filter(e => e.trim());
if (edges) { if (allEdges.length > 0) {
parts.push(edges); const abstractWrapper = buildAbstractWrapper(allEdges.join("\n"));
if (abstractWrapper) {
parts.push(abstractWrapper);
}
} }
return parts.join("\n\n"); 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<string, string[]>();
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. * Generiert Edge-Callouts für explizite Referenzen.
* Format: > [!edge] <edge_type>\n> [[#^block-id]] * Format: > [!edge] <edge_type>\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( function renderReferences(
references: Array<{ block_id: string; edge_type?: string }>, references: Array<{ block_id: string; edge_type?: string }>,
@ -585,35 +807,98 @@ function renderReferences(
const edgeCallouts: string[] = []; const edgeCallouts: string[] = [];
for (const ref of references) { 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)) { if (!context.generatedBlockIds.has(ref.block_id)) {
// 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`); console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`);
continue; continue;
} }
}
// WP-26: Prüfe auf Selbstreferenz // WP-26: Prüfe auf Selbstreferenz
if (ref.block_id === currentSection.blockId) { if (resolvedBlockId === currentSection.blockId) {
console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${ref.block_id}" zeigt auf sich selbst`); console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${resolvedBlockId}" zeigt auf sich selbst`);
continue; continue;
} }
const edgeType = ref.edge_type || "related_to"; const edgeType = ref.edge_type || "related_to";
edgeCallouts.push(`> [!edge] ${edgeType}`); edgeCallouts.push(`> [!edge] ${edgeType}`);
edgeCallouts.push(`> [[#^${ref.block_id}]]`); edgeCallouts.push(`> [[#^${resolvedBlockId}]]`);
} }
return edgeCallouts.join("\n"); return edgeCallouts.join("\n");
} }
/** /**
* Generiert automatische Edge-Vorschläge zwischen Sections. * Ermittelt den effektiven Section-Type basierend auf Heading-Level.
* Verwendet graph_schema.md für typische Edge-Types und edge_vocabulary.md für inverse Edges. * Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section
* * auf dem gleichen oder höheren Level verwendet, sonst Note-Type.
* 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)
*/ */
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, currentSection: SectionInfo,
context: RenderContext context: RenderContext
): string { ): string {
@ -622,43 +907,158 @@ function renderAutomaticEdges(
return ""; return "";
} }
// Nur wenn es vorherige Sections gibt // Finde Index der aktuellen Section in der Sequenz
if (context.sectionSequence.length <= 1) { const currentIndex = context.sectionSequence.findIndex(
return ""; 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 // ALLE vorherigen Sections (nicht nur die direkt vorherige)
// Wir generieren Edges von allen vorherigen Sections zur aktuellen Section const prevSections = context.sectionSequence.slice(0, currentIndex);
const prevSections = context.sectionSequence.slice(0, -1);
const edgeCallouts: string[] = []; 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 // Nur wenn vorherige Section auch eine Block-ID hat
if (!prevSection.blockId) { if (!prevSection || !prevSection.blockId) {
continue; 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) { if (prevSection.blockId === currentSection.blockId) {
console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${prevSection.blockId}" zeigt auf sich selbst`); console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${prevSection.blockId}" zeigt auf sich selbst`);
continue; continue;
} }
// Ermittle effective_types // WP-26: Auflösung der Block-ID für Loop-Items
const prevType = prevSection.sectionType || prevSection.noteType; // Prüfe zuerst, ob die Block-ID direkt existiert
const currentType = currentSection.sectionType || currentSection.noteType; 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;
}
}
// 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 // Lookup in graph_schema.md: prevType -> currentType
let forwardEdgeType: string | null = null;
if (graphSchema && prevType && currentType) { if (graphSchema && prevType && currentType) {
const hints = getHints(graphSchema, prevType, currentType); const hints = getHints(graphSchema, prevType, currentType);
if (hints.typical.length > 0) { if (hints.typical.length > 0) {
forwardEdgeType = hints.typical[0]; // Erster typischer Edge-Type aus graph_schema forwardEdgeType = hints.typical[0] || null;
} else { } else {
// Debug: Log wenn keine typischen Edges gefunden wurden console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}, verwende Fallback`);
console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}`); }
}
// 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] || null;
} }
} }
@ -666,32 +1066,136 @@ function renderAutomaticEdges(
if (!forwardEdgeType) { if (!forwardEdgeType) {
forwardEdgeType = "related_to"; forwardEdgeType = "related_to";
} }
}
// WP-26: Automatische Edge-Vorschläge // WP-26: Backward-Edge generieren (currentSection -> prevSection)
// Wir generieren beide Richtungen: // Das ist die inverse Edge zu der Forward-Edge (prevSection -> currentSection)
// 1. Forward-Edge: prevSection -> currentSection (mit originalem Edge-Type) let backwardEdgeType: string = forwardEdgeType;
// 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) { if (vocabulary) {
inverseEdgeType = vocabulary.getInverse(forwardEdgeType); const inverse = vocabulary.getInverse(forwardEdgeType);
if (inverse) {
backwardEdgeType = inverse;
}
} }
// Generiere Rückwärts-Edge (currentSection -> prevSection) // WP-26: Backward-Edge (currentSection -> prevSection) wird in prevSection eingefügt
if (inverseEdgeType) { // Diese Edge zeigt von currentSection zurück zu prevSection
edgeCallouts.push(`> [!edge] ${inverseEdgeType}`); return `> [!edge] ${backwardEdgeType}\n> [[#^${currentSection.blockId}]]`;
edgeCallouts.push(`> [[#^${prevSection.blockId}]]`);
} }
// Generiere Forward-Edge als Backlink (prevSection -> currentSection) /**
// Diese Edge beschreibt die Beziehung von prevSection zu currentSection * Fügt Edges zu einem bestehenden abstract wrapper hinzu oder erstellt einen neuen.
edgeCallouts.push(`> [!edge] ${forwardEdgeType}`); */
edgeCallouts.push(`> [[#^${prevSection.blockId}]]`); function addEdgesToAbstractWrapper(content: string, newEdges: string): string {
if (!newEdges.trim()) {
return content;
} }
return edgeCallouts.join("\n"); // 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<string, string[]>();
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");
} }

View File

@ -11,6 +11,15 @@ export interface PendingEdgeAssignment {
createdAt: number; 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 { export interface WizardState {
profile: InterviewProfile; profile: InterviewProfile;
currentStepIndex: number; currentStepIndex: number;
@ -21,6 +30,9 @@ export interface WizardState {
patches: Patch[]; // Collected patches to apply patches: Patch[]; // Collected patches to apply
activeLoopPath: string[]; // Stack of loop keys representing current nesting level (e.g. ["items", "item_list"]) activeLoopPath: string[]; // Stack of loop keys representing current nesting level (e.g. ["items", "item_list"])
pendingEdgeAssignments: PendingEdgeAssignment[]; // Inline micro edge assignments collected during wizard pendingEdgeAssignments: PendingEdgeAssignment[]; // Inline micro edge assignments collected during wizard
// WP-26: Section-Type und Block-ID Tracking
generatedBlockIds: Map<string, SectionInfo>;
sectionSequence: SectionInfo[];
} }
export interface Patch { export interface Patch {

View File

@ -220,6 +220,19 @@ export async function buildSemanticMappings(
// Process each link in worklist // Process each link in worklist
for (const item of worklist.items) { 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) // Always prompt user (even for existing mappings)
// Keep is the default option if currentType exists // Keep is the default option if currentType exists
const prompt = new LinkPromptModal(app, item, vocabulary, sourceType, graphSchema); const prompt = new LinkPromptModal(app, item, vocabulary, sourceType, graphSchema);

View File

@ -43,7 +43,32 @@ export class EdgeTypeChooserModal extends Modal {
contentEl.empty(); contentEl.empty();
contentEl.addClass("edge-type-chooser-modal"); 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 // Get suggestions
const suggestions = computeEdgeSuggestions( const suggestions = computeEdgeSuggestions(

View File

@ -9,6 +9,7 @@ import type { EdgeVocabulary } from "../vocab/types";
import type { GraphSchema } from "../mapping/graphSchema"; import type { GraphSchema } from "../mapping/graphSchema";
import type { MindnetSettings } from "../settings"; import type { MindnetSettings } from "../settings";
import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal"; import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal";
import { getHints } from "../mapping/graphSchema";
export interface InlineEdgeTypeResult { export interface InlineEdgeTypeResult {
chosenRawType: string | null; // null means skip chosenRawType: string | null; // null means skip
@ -68,6 +69,31 @@ export class InlineEdgeTypeModal extends Modal {
}); });
linkInfo.textContent = `Link: [[${this.linkBasename}]]`; 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) // Show selected type if one was chosen (e.g., from chooser)
if (this.selectedEdgeType) { if (this.selectedEdgeType) {
const selectedInfo = contentEl.createEl("div", { const selectedInfo = contentEl.createEl("div", {
@ -450,34 +476,40 @@ export class InlineEdgeTypeModal extends Modal {
const alternatives: string[] = []; const alternatives: string[] = [];
const prohibited: 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) { if (this.graphSchema && this.sourceType && this.targetType) {
const sourceMap = this.graphSchema.schema.get(this.sourceType); const hints = getHints(this.graphSchema, this.sourceType, this.targetType);
if (sourceMap) {
const hints = sourceMap.get(this.targetType);
if (hints) {
typical.push(...hints.typical); typical.push(...hints.typical);
prohibited.push(...hints.prohibited); 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 // Compute alternatives: limit to a reasonable number of common edge types
if (typical.length === 0 && this.vocabulary) { // 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 // Get top common edge types from vocabulary
// EdgeVocabulary has byCanonical: Map<CanonicalEdgeType, EdgeTypeEntry>
const edgeTypes = Array.from(this.vocabulary.byCanonical.keys()); const edgeTypes = Array.from(this.vocabulary.byCanonical.keys());
// Sort by usage or just take first N
const maxAlternatives = this.settings.inlineMaxAlternatives || 6; const maxAlternatives = this.settings.inlineMaxAlternatives || 6;
alternatives.push(...edgeTypes.slice(0, maxAlternatives)); 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 }; return { typical, alternatives, prohibited };
} }
} }

View File

@ -37,6 +37,7 @@ import { insertWikilinkIntoTextarea } from "../entityPicker/wikilink";
import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMappingBuilder"; import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMappingBuilder";
import type { MindnetSettings } from "../settings"; import type { MindnetSettings } from "../settings";
import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal"; import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal";
import { SectionEdgesOverviewModal, type SectionEdgesOverviewResult } from "./SectionEdgesOverviewModal";
import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver"; import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver";
import type { PendingEdgeAssignment } from "../interview/wizardState"; import type { PendingEdgeAssignment } from "../interview/wizardState";
import { VocabularyLoader } from "../vocab/VocabularyLoader"; import { VocabularyLoader } from "../vocab/VocabularyLoader";
@ -186,6 +187,21 @@ export class InterviewWizardModal extends Modal {
// WP-26: Track Section-Info während des Wizard-Durchlaufs // WP-26: Track Section-Info während des Wizard-Durchlaufs
if (step) { 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}`); console.log(`[WP-26] trackSectionInfo wird aufgerufen für Step ${step.key}, type: ${step.type}`);
this.trackSectionInfo(step); this.trackSectionInfo(step);
} else { } else {
@ -433,7 +449,7 @@ export class InterviewWizardModal extends Modal {
let hadFocus = false; let hadFocus = false;
let selectionStart = 0; let selectionStart = 0;
let selectionEnd = 0; let selectionEnd = 0;
if (document.activeElement === textareaRef) { if (textareaRef && document.activeElement === textareaRef) {
hadFocus = true; hadFocus = true;
selectionStart = textareaRef.selectionStart; selectionStart = textareaRef.selectionStart;
selectionEnd = textareaRef.selectionEnd; selectionEnd = textareaRef.selectionEnd;
@ -1549,7 +1565,7 @@ export class InterviewWizardModal extends Modal {
text.onChange((value) => { text.onChange((value) => {
// WP-26: Speichere Fokus-Info vor State-Update // WP-26: Speichere Fokus-Info vor State-Update
if (document.activeElement === textareaRef) { if (textareaRef && document.activeElement === textareaRef) {
hadFocus = true; hadFocus = true;
selectionStart = textareaRef.selectionStart; selectionStart = textareaRef.selectionStart;
selectionEnd = textareaRef.selectionEnd; selectionEnd = textareaRef.selectionEnd;
@ -2532,9 +2548,73 @@ export class InterviewWizardModal extends Modal {
if (currentStep) { if (currentStep) {
this.saveCurrentStepData(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<string, Map<string, string>> | 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; const edgingMode = this.profile.edging?.mode;
console.log("[Mindnet] Checking edging mode:", { console.log("[Mindnet] Checking edging mode:", {
profileKey: this.profile.key, profileKey: this.profile.key,
@ -2544,12 +2624,14 @@ export class InterviewWizardModal extends Modal {
}); });
// Support: post_run, both (inline_micro + post_run) // 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) { if (shouldRunPostRun) {
console.log("[Mindnet] Starting post-run edging"); console.log("[Mindnet] Starting post-run edging für Note-Edges");
await this.runPostRunEdging(); await this.runPostRunEdging();
} else { } 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 }); this.onSubmit({ applied: true, patches: this.state.patches });
@ -2565,9 +2647,9 @@ export class InterviewWizardModal extends Modal {
}); });
}) })
.addButton((button) => { .addButton((button) => {
button.setButtonText("Save & Exit").onClick(() => { button.setButtonText("Save & Exit").onClick(async () => {
console.log("=== SAVE & EXIT ==="); console.log("=== SAVE & EXIT ===");
this.applyPatches(); await this.applyPatches();
this.onSaveAndExit({ this.onSaveAndExit({
applied: true, applied: true,
patches: this.state.patches, patches: this.state.patches,
@ -2690,9 +2772,16 @@ export class InterviewWizardModal extends Modal {
if (currentBlockId) { if (currentBlockId) {
const sourceSection = this.state.generatedBlockIds.get(currentBlockId); const sourceSection = this.state.generatedBlockIds.get(currentBlockId);
if (sourceSection) { 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; sourceType = sourceSection.sectionType || sourceSection.noteType;
console.log(`[WP-26] Source-Type aus Section: ${sourceType}`); console.log(`[WP-26] Source-Type aus Section: ${sourceType}`);
} }
}
} else { } else {
// Fallback: Verwende Note-Type // Fallback: Verwende Note-Type
sourceType = this.state.profile.note_type; sourceType = this.state.profile.note_type;
@ -2716,7 +2805,59 @@ export class InterviewWizardModal extends Modal {
targetNoteId = sourceNoteId; // Gleiche Note targetNoteId = sourceNoteId; // Gleiche Note
} else { } else {
// Inter-Note-Edge: Normale Wikilink-Referenz // Inter-Note-Edge: Normale Wikilink-Referenz
// Get source note ID and type // 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 sourceContent = this.fileContent;
const { extractFrontmatterId } = await import("../parser/parseFrontmatter"); const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
sourceNoteId = extractFrontmatterId(sourceContent) || undefined; sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
@ -2725,8 +2866,15 @@ export class InterviewWizardModal extends Modal {
const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m); const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m);
if (typeMatch && typeMatch[1]) { if (typeMatch && typeMatch[1]) {
sourceType = typeMatch[1].trim(); 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 // Get target note ID and type
try { try {
@ -3052,7 +3200,7 @@ export class InterviewWizardModal extends Modal {
}; };
} }
async applyPatches(): Promise<void> { async applyPatches(sectionEdgeTypes?: Map<string, Map<string, string>>): Promise<void> {
console.log("=== APPLY PATCHES ===", { console.log("=== APPLY PATCHES ===", {
patchCount: this.state.patches.length, patchCount: this.state.patches.length,
patches: this.state.patches.map(p => ({ patches: this.state.patches.map(p => ({
@ -3130,8 +3278,20 @@ export class InterviewWizardModal extends Modal {
graphSchema: graphSchema, graphSchema: graphSchema,
vocabulary: vocabulary, vocabulary: vocabulary,
noteType: this.state.profile.note_type, 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); const renderedMarkdown = renderProfileToMarkdown(this.state.profile, answers, renderOptions);
if (renderedMarkdown.trim()) { if (renderedMarkdown.trim()) {
@ -3142,6 +3302,9 @@ export class InterviewWizardModal extends Modal {
contentLength: renderedMarkdown.length, contentLength: renderedMarkdown.length,
preview: renderedMarkdown.substring(0, 200) + "...", 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 // Write updated content
@ -3224,25 +3387,36 @@ export class InterviewWizardModal extends Modal {
return; 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 // Für capture_text: Nur wenn Section vorhanden ist
if (nestedStep.type === "capture_text" && !captureStep.section) { if (isCaptureText && !captureTextStep?.section) {
return; return;
} }
// Für capture_text_line: Nur wenn heading_level enabled ist // 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; return;
} }
// Verwende die richtige Step-Variable für die weitere Verarbeitung
const captureStep = isCaptureText ? captureTextStep! : captureTextLineStep!;
// WP-26: Block-ID ermitteln // 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; let blockId: string | null = null;
if (captureStep.block_id) { if (captureStep.block_id) {
blockId = captureStep.block_id; blockId = captureStep.block_id;
} else if (captureStep.generate_block_id) { } else if (captureStep.generate_block_id) {
// Für Loop-Items: Verwende Loop-Key + Step-Key für eindeutige Block-ID // Für Loop-Items: Verwende nur Step-Key (Index wird im Renderer hinzugefügt)
blockId = slugify(`${loopKey}-${captureStep.key}`); // Die tatsächliche Block-ID wird im Renderer mit Item-Index generiert
blockId = slugify(captureStep.key);
} }
// WP-26: Section-Type ermitteln // WP-26: Section-Type ermitteln
@ -3251,10 +3425,10 @@ export class InterviewWizardModal extends Modal {
// WP-26: Heading-Text ermitteln // WP-26: Heading-Text ermitteln
let heading = ""; let heading = "";
if (nestedStep.type === "capture_text" && captureStep.section) { if (isCaptureText && captureTextStep?.section) {
// Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext") // Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext")
heading = captureStep.section.replace(/^#+\s+/, ""); heading = captureTextStep.section.replace(/^#+\s+/, "");
} else if (nestedStep.type === "capture_text_line") { } else if (isCaptureTextLine) {
// Für capture_text_line: Heading aus Draft-Wert // Für capture_text_line: Heading aus Draft-Wert
const draftValue = draft[captureStep.key]; const draftValue = draft[captureStep.key];
if (draftValue && typeof draftValue === "string") { if (draftValue && typeof draftValue === "string") {
@ -3306,32 +3480,40 @@ export class InterviewWizardModal extends Modal {
return; 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}`, { console.log(`[WP-26] trackSectionInfo aufgerufen für Step ${step.key}`, {
type: step.type, type: step.type,
hasSection: !!captureStep.section, hasSection: isCaptureText ? !!captureTextStep?.section : false,
section: captureStep.section, section: isCaptureText ? captureTextStep?.section : undefined,
hasSectionType: !!captureStep.section_type, hasSectionType: isCaptureText ? !!captureTextStep?.section_type : !!captureTextLineStep?.section_type,
sectionType: captureStep.section_type, sectionType: isCaptureText ? captureTextStep?.section_type : captureTextLineStep?.section_type,
hasBlockId: !!captureStep.block_id, hasBlockId: isCaptureText ? !!captureTextStep?.block_id : !!captureTextLineStep?.block_id,
blockId: captureStep.block_id, blockId: isCaptureText ? captureTextStep?.block_id : captureTextLineStep?.block_id,
generateBlockId: captureStep.generate_block_id, generateBlockId: isCaptureText ? captureTextStep?.generate_block_id : captureTextLineStep?.generate_block_id,
hasHeadingLevel: !!captureStep.heading_level?.enabled, hasHeadingLevel: isCaptureTextLine ? !!captureTextLineStep?.heading_level?.enabled : false,
}); });
// Nur wenn Section vorhanden ist // Nur wenn Section vorhanden ist (für capture_text)
if (!captureStep.section && step.type === "capture_text") { if (isCaptureText && !captureTextStep?.section) {
console.log(`[WP-26] Step ${step.key} hat keine section, überspringe`); console.log(`[WP-26] Step ${step.key} hat keine section, überspringe`);
return; return;
} }
// Für capture_text_line: Nur wenn heading_level enabled ist // 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`); console.log(`[WP-26] Step ${step.key} hat kein heading_level enabled, überspringe`);
return; return;
} }
// Verwende die richtige Step-Variable für die weitere Verarbeitung
const captureStep = isCaptureText ? captureTextStep! : captureTextLineStep!;
// WP-26: Block-ID ermitteln // WP-26: Block-ID ermitteln
let blockId: string | null = null; let blockId: string | null = null;
if (captureStep.block_id) { if (captureStep.block_id) {
@ -3346,13 +3528,13 @@ export class InterviewWizardModal extends Modal {
// WP-26: Heading-Text ermitteln // WP-26: Heading-Text ermitteln
let heading = ""; let heading = "";
if (step.type === "capture_text" && captureStep.section) { if (isCaptureText && captureTextStep?.section) {
// Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext") // Extrahiere Heading-Text aus Section (z.B. "## 📖 Kontext" -> "📖 Kontext")
heading = captureStep.section.replace(/^#+\s+/, ""); heading = captureTextStep.section.replace(/^#+\s+/, "");
} else if (step.type === "capture_text_line") { } else if (isCaptureTextLine) {
// Für capture_text_line: Heading wird später aus dem eingegebenen Text generiert // Für capture_text_line: Heading wird später aus dem eingegebenen Text generiert
// Verwende Step-Key als Platzhalter // Verwende Step-Key als Platzhalter
heading = captureStep.key; heading = step.key;
} }
// WP-26: Section-Info erstellen // 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 { onClose(): void {
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();

View File

@ -67,8 +67,29 @@ export class LinkPromptModal extends Modal {
const linkDisplayText = this.item.displayLink || this.item.link; const linkDisplayText = this.item.displayLink || this.item.link;
linkInfo.createEl("h2", { text: `Link: [[${linkDisplayText}]]` }); linkInfo.createEl("h2", { text: `Link: [[${linkDisplayText}]]` });
if (this.item.targetType) { // WP-26: Zeige Quell- und Ziel-Typ für Übersichtlichkeit
linkInfo.createEl("p", { text: `Target type: ${this.item.targetType}` }); 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 // Show current/selected edge type prominently

View File

@ -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<string, Map<string, string>>; // fromBlockId -> (toBlockId -> edgeType)
noteEdges: Map<string, Map<string, string>>; // 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<string, unknown>; // 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<string, unknown>
) {
super(app);
this.sectionSequence = sectionSequence;
this.vocabulary = vocabulary;
this.graphSchema = graphSchema;
this.collectedData = collectedData || new Map();
}
async onOpen(): Promise<void> {
// 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<string, SectionEdge[]>();
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<string | null, NoteEdge[]>();
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<void> {
this.noteEdges = [];
// Extrahiere alle Note-Links aus gesammelten Daten
const noteLinksBySection = new Map<string | null, Set<string>>(); // blockId -> Set<noteBasename>
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<void> {
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<void> {
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<string, Map<string, string>>();
const noteEdgesMap = new Map<string, Map<string, string>>();
// 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<SectionEdgesOverviewResult> {
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;
}
}
}