Enhance interview configuration and documentation for WP-26 integration
- 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:
parent
b99416b67d
commit
8186ca5ce0
|
|
@ -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)
|
||||||
|
|
@ -167,7 +168,8 @@
|
||||||
|
|
||||||
### 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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
857
docs/Interview_Config_Guide.md
Normal file
857
docs/Interview_Config_Guide.md
Normal 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
146
docs/audit_geburtsdatei.md
Normal 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
|
||||||
|
|
@ -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\]/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
profile,
|
||||||
|
sectionSequence,
|
||||||
|
generatedBlockIds,
|
||||||
|
options: options || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// WP-26: Erster Durchlauf: Rendere alle Steps und sammle gerenderte Sections
|
||||||
|
const renderedSections: Array<{ sectionInfo: SectionInfo; content: string }> = [];
|
||||||
|
const output: string[] = [];
|
||||||
|
|
||||||
for (const step of profile.steps) {
|
for (const step of profile.steps) {
|
||||||
const stepOutput = renderStep(
|
// WP-26: Debug-Log für Steps mit WP-26 Feldern
|
||||||
step,
|
if (step.type === "capture_text" || step.type === "capture_text_line") {
|
||||||
answers,
|
const captureStep = step as CaptureTextStep | CaptureTextLineStep;
|
||||||
{
|
console.log(`[WP-26] Renderer: Step ${step.key} hat WP-26 Felder:`, {
|
||||||
profile,
|
section_type: captureStep.section_type,
|
||||||
sectionSequence,
|
block_id: captureStep.block_id,
|
||||||
generatedBlockIds,
|
generate_block_id: captureStep.generate_block_id,
|
||||||
options: options || {},
|
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)) {
|
||||||
console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`);
|
// Versuche, Block-ID mit Index aufzulösen (für Loop-Items)
|
||||||
continue;
|
// Suche nach Block-IDs, die mit der Basis-Block-ID beginnen
|
||||||
|
let foundBlockId: string | null = null;
|
||||||
|
for (const [blockId] of context.generatedBlockIds.entries()) {
|
||||||
|
// Prüfe, ob Block-ID mit Basis-Block-ID beginnt (z.B. "action_heading-1", "action_heading-2")
|
||||||
|
if (blockId.startsWith(`${ref.block_id}-`)) {
|
||||||
|
// Verwende die neueste gefundene Block-ID (höchster Index)
|
||||||
|
if (!foundBlockId || blockId > foundBlockId) {
|
||||||
|
foundBlockId = blockId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundBlockId) {
|
||||||
|
resolvedBlockId = foundBlockId;
|
||||||
|
} else {
|
||||||
|
console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WP-26: Prüfe auf Selbstreferenz
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lookup in graph_schema.md: prevType -> currentType
|
// WP-26: Prüfe zuerst, ob ein manuell geänderter Edge-Type vorhanden ist
|
||||||
let forwardEdgeType: string | null = null;
|
let forwardEdgeType: string | null = null;
|
||||||
|
if (context.options.sectionEdgeTypes) {
|
||||||
|
const fromBlockId = prevSection.blockId;
|
||||||
|
const toBlockId = currentSection.blockId;
|
||||||
|
if (fromBlockId && toBlockId) {
|
||||||
|
const fromMap = context.options.sectionEdgeTypes.get(fromBlockId);
|
||||||
|
if (fromMap) {
|
||||||
|
forwardEdgeType = fromMap.get(toBlockId) || null;
|
||||||
|
if (forwardEdgeType) {
|
||||||
|
console.log(`[WP-26] Verwende manuell geänderten Edge-Type: ${fromBlockId} -> ${toBlockId} = ${forwardEdgeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls kein manuell geänderter Edge-Type vorhanden, verwende graph_schema
|
||||||
|
if (!forwardEdgeType) {
|
||||||
|
// WP-26: Ermittle effective_types mit Heading-Level-basierter Fallback-Logik
|
||||||
|
const prevType = getEffectiveSectionType(prevSection, prevIdx, context.sectionSequence);
|
||||||
|
const currentType = getEffectiveSectionType(currentSection, currentIndex, context.sectionSequence);
|
||||||
|
|
||||||
|
// Lookup in graph_schema.md: prevType -> currentType
|
||||||
|
if (graphSchema && prevType && currentType) {
|
||||||
|
const hints = getHints(graphSchema, prevType, currentType);
|
||||||
|
if (hints.typical.length > 0) {
|
||||||
|
forwardEdgeType = hints.typical[0] || null;
|
||||||
|
} else {
|
||||||
|
console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}, verwende Fallback`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: related_to für experience -> experience, references für andere
|
||||||
|
if (!forwardEdgeType) {
|
||||||
|
if (prevType === currentType && prevType === "experience") {
|
||||||
|
forwardEdgeType = "related_to";
|
||||||
|
} else {
|
||||||
|
forwardEdgeType = "references";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WP-26: Forward-Edge generieren (prevSection -> currentSection)
|
||||||
|
// Diese Edge wird in der aktuellen Section eingefügt
|
||||||
|
edgeCallouts.push(`> [!edge] ${forwardEdgeType}`);
|
||||||
|
edgeCallouts.push(`> [[#^${resolvedPrevBlockId}]]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return edgeCallouts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert eine einzelne Backward-Edge zwischen zwei Sections.
|
||||||
|
* Backward-Edge: currentSection -> prevSection (wird in prevSection eingefügt)
|
||||||
|
*
|
||||||
|
* Wenn Section A -> Section B (Forward-Edge), dann wird Backward-Edge (B -> A) in Section A eingefügt.
|
||||||
|
*/
|
||||||
|
function renderSingleBackwardEdge(
|
||||||
|
currentSection: SectionInfo,
|
||||||
|
prevSection: SectionInfo,
|
||||||
|
currentIndex: number,
|
||||||
|
prevIndex: number,
|
||||||
|
context: RenderContext
|
||||||
|
): string {
|
||||||
|
// Nur wenn beide Sections Block-IDs haben
|
||||||
|
if (!currentSection.blockId || !prevSection.blockId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// WP-26: Prüfe auf Selbstreferenz
|
||||||
|
if (prevSection.blockId === currentSection.blockId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { graphSchema, vocabulary } = context.options;
|
||||||
|
|
||||||
|
// WP-26: Prüfe zuerst, ob ein manuell geänderter Edge-Type vorhanden ist
|
||||||
|
let forwardEdgeType: string | null = null;
|
||||||
|
if (context.options.sectionEdgeTypes) {
|
||||||
|
const fromBlockId = prevSection.blockId;
|
||||||
|
const toBlockId = currentSection.blockId;
|
||||||
|
if (fromBlockId && toBlockId) {
|
||||||
|
const fromMap = context.options.sectionEdgeTypes.get(fromBlockId);
|
||||||
|
if (fromMap) {
|
||||||
|
forwardEdgeType = fromMap.get(toBlockId) || null;
|
||||||
|
if (forwardEdgeType) {
|
||||||
|
console.log(`[WP-26] Verwende manuell geänderten Edge-Type für Backward-Edge: ${fromBlockId} -> ${toBlockId} = ${forwardEdgeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls kein manuell geänderter Edge-Type vorhanden, verwende graph_schema
|
||||||
|
if (!forwardEdgeType) {
|
||||||
|
// WP-26: Ermittle effective_types mit Heading-Level-basierter Fallback-Logik
|
||||||
|
const prevType = getEffectiveSectionType(prevSection, prevIndex, context.sectionSequence);
|
||||||
|
const currentType = getEffectiveSectionType(currentSection, currentIndex, context.sectionSequence);
|
||||||
|
|
||||||
|
// Lookup in graph_schema.md: prevType -> currentType (Forward-Edge-Type)
|
||||||
if (graphSchema && prevType && currentType) {
|
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 {
|
|
||||||
// Debug: Log wenn keine typischen Edges gefunden wurden
|
|
||||||
console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,32 +1066,136 @@ function renderAutomaticEdges(
|
||||||
if (!forwardEdgeType) {
|
if (!forwardEdgeType) {
|
||||||
forwardEdgeType = "related_to";
|
forwardEdgeType = "related_to";
|
||||||
}
|
}
|
||||||
|
|
||||||
// WP-26: Automatische Edge-Vorschläge
|
|
||||||
// Wir generieren beide Richtungen:
|
|
||||||
// 1. Forward-Edge: prevSection -> currentSection (mit originalem Edge-Type)
|
|
||||||
// 2. Rückwärts-Edge: currentSection -> prevSection (mit inversem Edge-Type)
|
|
||||||
//
|
|
||||||
// Beide Edges werden in der aktuellen Section eingefügt und zeigen zur prevSection
|
|
||||||
// Der Backlink wird automatisch mitgesetzt
|
|
||||||
|
|
||||||
// Rückwärts-Edge: currentSection -> prevSection (mit inversem Edge-Type)
|
|
||||||
let inverseEdgeType: string | null = null;
|
|
||||||
if (vocabulary) {
|
|
||||||
inverseEdgeType = vocabulary.getInverse(forwardEdgeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generiere Rückwärts-Edge (currentSection -> prevSection)
|
|
||||||
if (inverseEdgeType) {
|
|
||||||
edgeCallouts.push(`> [!edge] ${inverseEdgeType}`);
|
|
||||||
edgeCallouts.push(`> [[#^${prevSection.blockId}]]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generiere Forward-Edge als Backlink (prevSection -> currentSection)
|
|
||||||
// Diese Edge beschreibt die Beziehung von prevSection zu currentSection
|
|
||||||
edgeCallouts.push(`> [!edge] ${forwardEdgeType}`);
|
|
||||||
edgeCallouts.push(`> [[#^${prevSection.blockId}]]`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return edgeCallouts.join("\n");
|
// WP-26: Backward-Edge generieren (currentSection -> prevSection)
|
||||||
|
// Das ist die inverse Edge zu der Forward-Edge (prevSection -> currentSection)
|
||||||
|
let backwardEdgeType: string = forwardEdgeType;
|
||||||
|
if (vocabulary) {
|
||||||
|
const inverse = vocabulary.getInverse(forwardEdgeType);
|
||||||
|
if (inverse) {
|
||||||
|
backwardEdgeType = inverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WP-26: Backward-Edge (currentSection -> prevSection) wird in prevSection eingefügt
|
||||||
|
// Diese Edge zeigt von currentSection zurück zu prevSection
|
||||||
|
return `> [!edge] ${backwardEdgeType}\n> [[#^${currentSection.blockId}]]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt Edges zu einem bestehenden abstract wrapper hinzu oder erstellt einen neuen.
|
||||||
|
*/
|
||||||
|
function addEdgesToAbstractWrapper(content: string, newEdges: string): string {
|
||||||
|
if (!newEdges.trim()) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob bereits ein abstract wrapper vorhanden ist
|
||||||
|
const abstractWrapperMatch = content.match(/(> \[!abstract\][^\n]*\n(?:>>?[^\n]*\n)*> \^map-\d+)/);
|
||||||
|
|
||||||
|
if (abstractWrapperMatch && abstractWrapperMatch[1]) {
|
||||||
|
// Füge neue Edges zum bestehenden wrapper hinzu
|
||||||
|
const existingWrapper = abstractWrapperMatch[1];
|
||||||
|
const updatedWrapper = mergeEdgesIntoWrapper(existingWrapper, newEdges);
|
||||||
|
if (abstractWrapperMatch[0]) {
|
||||||
|
return content.replace(abstractWrapperMatch[0], updatedWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Erstelle neuen abstract wrapper
|
||||||
|
const abstractWrapper = buildAbstractWrapper(newEdges);
|
||||||
|
if (abstractWrapper) {
|
||||||
|
return content.trimEnd() + "\n\n" + abstractWrapper;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merged neue Edges in einen bestehenden abstract wrapper.
|
||||||
|
*/
|
||||||
|
function mergeEdgesIntoWrapper(existingWrapper: string, newEdges: string): string {
|
||||||
|
// Parse bestehenden wrapper
|
||||||
|
const lines = existingWrapper.split("\n");
|
||||||
|
const wrapperHeader = lines[0] || "> [!abstract]- 🕸️ Semantic Mapping";
|
||||||
|
const mapMarker = lines[lines.length - 1] || `> ^map-${Date.now()}`;
|
||||||
|
|
||||||
|
// Parse bestehende Edges
|
||||||
|
const existingGroups = new Map<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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
typical.push(...hints.typical);
|
||||||
const hints = sourceMap.get(this.targetType);
|
prohibited.push(...hints.prohibited);
|
||||||
if (hints) {
|
|
||||||
typical.push(...hints.typical);
|
console.log(`[WP-26] InlineEdgeTypeModal: Vorschläge für ${this.sourceType} -> ${this.targetType}:`, {
|
||||||
prohibited.push(...hints.prohibited);
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,8 +2772,15 @@ 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) {
|
||||||
sourceType = sourceSection.sectionType || sourceSection.noteType;
|
// WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik)
|
||||||
console.log(`[WP-26] Source-Type aus Section: ${sourceType}`);
|
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 {
|
} else {
|
||||||
// Fallback: Verwende Note-Type
|
// Fallback: Verwende Note-Type
|
||||||
|
|
@ -2716,16 +2805,75 @@ 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 sourceContent = this.fileContent;
|
const currentStep = step as import("../interview/types").CaptureTextStep | import("../interview/types").CaptureTextLineStep;
|
||||||
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
let currentBlockId: string | null = null;
|
||||||
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
if (currentStep.block_id) {
|
||||||
const sourceFrontmatter = sourceContent.match(/^---\n([\s\S]*?)\n---/);
|
currentBlockId = currentStep.block_id;
|
||||||
if (sourceFrontmatter && sourceFrontmatter[1]) {
|
} else if (currentStep.generate_block_id) {
|
||||||
const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m);
|
currentBlockId = slugify(currentStep.key);
|
||||||
if (typeMatch && typeMatch[1]) {
|
}
|
||||||
sourceType = typeMatch[1].trim();
|
|
||||||
|
// Versuche Section-Type zu ermitteln
|
||||||
|
if (currentBlockId) {
|
||||||
|
const sourceSection = this.state.generatedBlockIds.get(currentBlockId);
|
||||||
|
if (sourceSection) {
|
||||||
|
// WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik)
|
||||||
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.blockId === currentBlockId);
|
||||||
|
if (sectionIndex >= 0) {
|
||||||
|
sourceType = this.getEffectiveSectionType(sourceSection, sectionIndex);
|
||||||
|
console.log(`[WP-26] Source-Type aus Section (effektiv): ${sourceType}`);
|
||||||
|
} else {
|
||||||
|
sourceType = sourceSection.sectionType || sourceSection.noteType;
|
||||||
|
console.log(`[WP-26] Source-Type aus Section: ${sourceType}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Versuche Section-Type aus sectionSequence zu finden (auch wenn keine Block-ID vorhanden)
|
||||||
|
const sectionInfo = this.state.sectionSequence.find(s => s.stepKey === step.key);
|
||||||
|
if (sectionInfo) {
|
||||||
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.stepKey === step.key);
|
||||||
|
if (sectionIndex >= 0) {
|
||||||
|
sourceType = this.getEffectiveSectionType(sectionInfo, sectionIndex);
|
||||||
|
console.log(`[WP-26] Source-Type aus sectionSequence (effektiv): ${sourceType}`);
|
||||||
|
} else {
|
||||||
|
sourceType = sectionInfo.sectionType || sectionInfo.noteType;
|
||||||
|
console.log(`[WP-26] Source-Type aus sectionSequence: ${sourceType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Versuche Section-Type aus sectionSequence zu finden (auch wenn keine Block-ID vorhanden)
|
||||||
|
const sectionInfo = this.state.sectionSequence.find(s => s.stepKey === step.key);
|
||||||
|
if (sectionInfo) {
|
||||||
|
const sectionIndex = this.state.sectionSequence.findIndex(s => s.stepKey === step.key);
|
||||||
|
if (sectionIndex >= 0) {
|
||||||
|
sourceType = this.getEffectiveSectionType(sectionInfo, sectionIndex);
|
||||||
|
console.log(`[WP-26] Source-Type aus sectionSequence (effektiv, kein Block-ID): ${sourceType}`);
|
||||||
|
} else {
|
||||||
|
sourceType = sectionInfo.sectionType || sectionInfo.noteType;
|
||||||
|
console.log(`[WP-26] Source-Type aus sectionSequence (kein Block-ID): ${sourceType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Verwende Note-Type aus Frontmatter
|
||||||
|
if (!sourceType) {
|
||||||
|
const sourceContent = this.fileContent;
|
||||||
|
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
||||||
|
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
||||||
|
const sourceFrontmatter = sourceContent.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
if (sourceFrontmatter && sourceFrontmatter[1]) {
|
||||||
|
const typeMatch = sourceFrontmatter[1].match(/^type:\s*(.+)$/m);
|
||||||
|
if (typeMatch && typeMatch[1]) {
|
||||||
|
sourceType = typeMatch[1].trim();
|
||||||
|
console.log(`[WP-26] Source-Type aus Frontmatter (Fallback): ${sourceType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// sourceNoteId bereits setzen, auch wenn Section-Type gefunden wurde
|
||||||
|
const sourceContent = this.fileContent;
|
||||||
|
const { extractFrontmatterId } = await import("../parser/parseFrontmatter");
|
||||||
|
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get target note ID and type
|
// Get target note ID and type
|
||||||
|
|
@ -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,31 +3480,39 @@ 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;
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
711
src/ui/SectionEdgesOverviewModal.ts
Normal file
711
src/ui/SectionEdgesOverviewModal.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user