Enhance UI and functionality for Chain Workbench and related features
- Introduced a wide two-column layout for the Chain Workbench modal, improving user experience and accessibility. - Added new styles for workbench components, including headers, filters, and main containers, to enhance visual organization. - Updated chain templates to allow for multiple distinct matches per template, improving flexibility in template matching. - Enhanced documentation to clarify the new settings and commands related to the Chain Workbench and edge detection features. - Implemented logging for better tracking of missing configurations, ensuring users are informed about any loading issues.
This commit is contained in:
parent
c044d6e8db
commit
725adb5302
|
|
@ -16,7 +16,9 @@ defaults:
|
|||
distinct_nodes: true
|
||||
required_links: true
|
||||
max_candidate_nodes: 30
|
||||
max_matches_per_template: 1
|
||||
# Max unterschiedliche Zuordnungen pro Template (z. B. 2 = intra-note + cross-note).
|
||||
# Bei großen Vaults evtl. 1 setzen; bei Bedarf erhöhen für mehr Varianten.
|
||||
max_matches_per_template: 2
|
||||
|
||||
# Rollen-Defaults (Referenz auf chain_roles.yaml Rollen)
|
||||
roles:
|
||||
|
|
@ -57,7 +59,8 @@ templates:
|
|||
allowed_edge_roles: [causal, influences, provenance]
|
||||
- from: transformation
|
||||
to: outcome
|
||||
allowed_edge_roles: [causal, influences, enables_constraints]
|
||||
# foundation_for (provenance) = Insight/Transformation begründet Outcome/Entscheidung
|
||||
allowed_edge_roles: [causal, influences, enables_constraints, provenance]
|
||||
|
||||
# ================================================================
|
||||
# 2) Core: Decision Logic (Driver + Constraint → Decision → Outcome)
|
||||
|
|
@ -91,7 +94,8 @@ templates:
|
|||
description: "Wiederkehrendes Muster: Experience → Insight/Principle → Behavior/Decision → neue Experience."
|
||||
slots:
|
||||
- id: experience
|
||||
allowed_node_types: [experience, journal, event]
|
||||
# situation: Section-Typ mancher Interview-Profile für „Situation“; erlaubt cross-note loop_learning.
|
||||
allowed_node_types: [experience, journal, event, situation]
|
||||
- id: learning
|
||||
allowed_node_types: [insight, principle, value, belief, skill, trait]
|
||||
- id: behavior
|
||||
|
|
@ -104,7 +108,8 @@ templates:
|
|||
allowed_edge_roles: [causal, provenance, influences]
|
||||
- from: learning
|
||||
to: behavior
|
||||
allowed_edge_roles: [influences, enables_constraints, causal]
|
||||
# foundation_for (provenance) = Einsicht begründet Entscheidung/Verhalten
|
||||
allowed_edge_roles: [influences, enables_constraints, causal, provenance]
|
||||
- from: behavior
|
||||
to: feedback
|
||||
allowed_edge_roles: [causal, influences]
|
||||
|
|
|
|||
|
|
@ -45,7 +45,19 @@ Das **Mindnet Causal Assistant** Plugin ist ein Authoring-Tool für Obsidian, da
|
|||
2. Stellen Sie sicher, dass "Restricted mode" ausgeschaltet ist
|
||||
3. Aktivieren Sie das Plugin "Mindnet Causal Assistant"
|
||||
|
||||
### 2. Erste Konfiguration
|
||||
### 2. Einstellungen finden
|
||||
|
||||
Die Plugin-Einstellungen erscheinen **nicht** unter „Community Plugins“ selbst, sondern wenn Sie **auf den Namen des Plugins** klicken:
|
||||
|
||||
1. **Einstellungen** öffnen (Zahnrad links unten oder **Settings**)
|
||||
2. In der **linken Leiste** **Community-Plugins** auswählen (falls nötig)
|
||||
3. In der Liste der installierten Plugins **auf „Mindnet Causal Assistant“ klicken**
|
||||
→ rechts erscheinen dann alle Einstellungen (Dictionary, Chain Inspector, Interview, etc.)
|
||||
|
||||
**Schnellzugriff:** Command Palette (`Ctrl+P` / `Cmd+P`) → **„Mindnet: Einstellungen öffnen“**
|
||||
Damit öffnen sich die Einstellungen; ggf. in der linken Leiste einmal auf „Mindnet Causal Assistant“ klicken.
|
||||
|
||||
### 3. Erste Konfiguration
|
||||
|
||||
Das Plugin benötigt Konfigurationsdateien im Vault. Diese sollten bereits vorhanden sein (siehe Administratorhandbuch). Standardpfade:
|
||||
|
||||
|
|
@ -55,7 +67,7 @@ Das Plugin benötigt Konfigurationsdateien im Vault. Diese sollten bereits vorha
|
|||
- `_system/dictionary/chain_roles.yaml` - Chain-Rollen-Mapping
|
||||
- `_system/dictionary/chain_templates.yaml` - Chain-Templates
|
||||
|
||||
### 3. Erste Note erstellen
|
||||
### 4. Erste Note erstellen
|
||||
|
||||
1. Öffnen Sie den Command Palette (`Ctrl+P` / `Cmd+P`)
|
||||
2. Wählen Sie **"Mindnet: Create note from profile"**
|
||||
|
|
|
|||
|
|
@ -781,7 +781,7 @@ npm run build
|
|||
|
||||
**Windows (PowerShell):**
|
||||
```bash
|
||||
npm run deploy:local
|
||||
npm.cmd run build:deploy
|
||||
# oder
|
||||
powershell -ExecutionPolicy Bypass -File scripts/deploy-local.ps1
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# LASTENHEFT: WP-26 Integration im Obsidian Plugin
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 25. Januar 2026
|
||||
**Status:** Entwurf zur Review
|
||||
**Version:** 1.1
|
||||
**Datum:** 28. Januar 2026
|
||||
**Status:** Entwurf zur Review (aktualisiert: Commands, Edge-Überarbeitung, Interview Edge-Typen pro Sektion)
|
||||
**Projekt:** mindnet_obsidian (Obsidian Community Plugin)
|
||||
**Basis:** Backend WP-26 v1.3 (Section Types & Intra-Note-Edges)
|
||||
|
||||
|
|
@ -84,11 +84,36 @@ Dieses Lastenheft definiert die vollständigen Anforderungen für die Integratio
|
|||
- Wizard-State muss Section-Types unterstützen
|
||||
- Section-Key-Resolver muss Block-IDs berücksichtigen
|
||||
|
||||
#### 2.1.4 Chain Workbench (`src/workbench/`)
|
||||
#### 2.1.4 Kausal-Funktionen (Chain Inspector, Chain Workbench)
|
||||
|
||||
**Überblick:** Die „Kausal-Funktionen“ bestehen aus (1) **Interview-Wizard** (geführte Erstellung von Notes mit kausal/argumentativen Strukturen), (2) **Chain Inspector** (Kausal-Analyse: Template-Matching, Findings wie `no_causal_roles`, `missing_link`), (3) **Chain Workbench** (UI für Findings, TODOs, Fix-Actions). Bis ca. Version 0.6.x waren diese ohne **Sektionsunterstützung mit unterschiedlichen Typen** (Section-Types); ab WP-26 wird Section-Type-basiertes Chain-Matching ergänzt (FA-PL-08).
|
||||
|
||||
**Aktuelle Funktionen / Module:**
|
||||
- `src/analysis/chainInspector.ts`: Analysiert kausale Ketten, Template-Matching, Findings (u. a. `no_causal_roles`, `weak_chain_roles`)
|
||||
- `src/workbench/`: `todoGenerator.ts`, `workbenchBuilder.ts`, `statusCalculator.ts`, `ChainWorkbenchModal.ts`
|
||||
- `src/commands/inspectChainsCommand.ts`: Command „Inspect Chains (Current Section)“
|
||||
- `src/commands/chainWorkbenchCommand.ts`: Command „Chain Workbench (Current Section)“
|
||||
- `src/commands/vaultTriageScanCommand.ts`: Command „Scan Vault for Chain Gaps“
|
||||
- Dictionary: `chain_roles.yaml` (Edge-Typen → kausale Rollen), `chain_templates.yaml` (Kettenmuster)
|
||||
|
||||
**Warum die Module „nicht aktiv“ erscheinen können:**
|
||||
|
||||
1. **Commands nur bei geöffneter Markdown-Datei:**
|
||||
„Inspect Chains“ und „Chain Workbench“ sind mit **editorCallback** registriert. In Obsidian erscheinen sie in der Befehlspalette nur, wenn eine **Markdown-Datei im Editor aktiv** ist. Ohne geöffnete Note sind sie ausgegraut bzw. nicht auffindbar – analog zu „Edge-Type ändern“ (siehe FA-PL-12a).
|
||||
|
||||
2. **Fehlende oder falsche Dictionary-Pfade:**
|
||||
Wenn `chain_roles.yaml` oder `chain_templates.yaml` am konfigurierten Pfad (Standard: `_system/dictionary/`) fehlen oder nicht lesbar sind, liefern die Loader `data === null`. Die Commands brechen **nicht** ab; Chain Inspector und Workbench laufen dann ohne Template-Matching und ohne kausale Rollen-Erkennung. Es erscheint **kein** expliziter Hinweis (z. B. „Chain Roles nicht geladen“). Das Ergebnis wirkt leer oder „inaktiv“.
|
||||
|
||||
3. **Sichtbarkeit in der Befehlspalette:**
|
||||
Ohne Hinweis in der Doku suchen Nutzer ggf. nach „Kausal“ oder „Wizard“; die registrierten Namen sind „Inspect Chains“ und „Chain Workbench“. Ein Eintrag im Lastenheft / Benutzerhandbuch verbessert die Auffindbarkeit.
|
||||
|
||||
**Lastenheft-Referenz:** Chain Workbench mit Section-Types ist in **FA-PL-08** beschrieben. Die allgemeine Sichtbarkeit von Commands (inkl. Kausal/Chain) ist in **FA-PL-12 / FA-PL-12a** adressiert.
|
||||
|
||||
#### 2.1.5 Chain Workbench – Module (`src/workbench/`)
|
||||
|
||||
**Aktuelle Funktionen:**
|
||||
- `todoGenerator.ts`: Generiert TODOs basierend auf Chain-Templates
|
||||
- `chainInspector.ts`: Analysiert Chains und findet Gaps
|
||||
- `chainInspector.ts` (in `src/analysis/`): Analysiert Chains und findet Gaps
|
||||
- `types.ts`: TypeScript-Interfaces für Workbench
|
||||
|
||||
**WP-26 Relevanz:**
|
||||
|
|
@ -96,7 +121,7 @@ Dieses Lastenheft definiert die vollständigen Anforderungen für die Integratio
|
|||
- Gap-Detection muss Intra-Note-Edges erkennen
|
||||
- Template-Matching muss effective types verwenden
|
||||
|
||||
#### 2.1.5 UI (`src/ui/`)
|
||||
#### 2.1.6 UI (`src/ui/`)
|
||||
|
||||
**Aktuelle Funktionen:**
|
||||
- `EdgeTypeChooserModal.ts`: Modal zur Edge-Type-Auswahl
|
||||
|
|
@ -109,7 +134,7 @@ Dieses Lastenheft definiert die vollständigen Anforderungen für die Integratio
|
|||
- Link-Prompt muss Block-ID-Generierung unterstützen
|
||||
- Interview-Wizard muss Section-Type-Auswahl ermöglichen
|
||||
|
||||
#### 2.1.6 Lint (`src/lint/`)
|
||||
#### 2.1.7 Lint (`src/lint/`)
|
||||
|
||||
**Aktuelle Funktionen:**
|
||||
- `LintEngine.ts`: Haupt-Lint-Engine
|
||||
|
|
@ -120,7 +145,7 @@ Dieses Lastenheft definiert die vollständigen Anforderungen für die Integratio
|
|||
- Validierung von Block-IDs
|
||||
- Validierung von Intra-Note-Edges
|
||||
|
||||
#### 2.1.7 Schema (`src/schema/`)
|
||||
#### 2.1.8 Schema (`src/schema/`)
|
||||
|
||||
**Aktuelle Funktionen:**
|
||||
- `GraphSchemaLoader.ts`: Lädt `graph_schema.md`
|
||||
|
|
@ -566,39 +591,103 @@ export interface SectionTypeLintRule {
|
|||
|
||||
### 3.5 Phase 5: Commands
|
||||
|
||||
#### FA-PL-12: Neue Commands
|
||||
#### FA-PL-12: Commands für WP-26 und Edge-Überarbeitung
|
||||
|
||||
**Anforderung:** Neue Commands für WP-26 Features.
|
||||
**Anforderung:** Commands für WP-26 Features und für die Edge-Überarbeitung in bestehenden Noten müssen in der Befehlspalette auffindbar und nutzbar sein.
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `src/main.ts` (erweitern)
|
||||
|
||||
**Neue Commands:**
|
||||
1. **"Mindnet: Add Section Type"**
|
||||
- Öffnet Section-Type-Modal
|
||||
- Fügt `[!section]` Callout ein
|
||||
**Bereits implementierte Commands (Stand: Jan 2026):**
|
||||
|
||||
2. **"Mindnet: Add Block ID to Heading"**
|
||||
- Generiert Block-ID für aktuelles Heading
|
||||
- Fügt `^block-id` zu Heading hinzu
|
||||
| Command-ID | Name (Befehlspalette) | Kontext | Modul / Funktion |
|
||||
|------------|------------------------|---------|-------------------|
|
||||
| `mindnet-change-edge-type` | Mindnet: Edge-Type ändern | **editorCallback** – nur bei geöffneter MD-Datei | `edgeTypeSelector.ts`: Einzellink, Auswahl, ganze Note, neuer Link |
|
||||
| `mindnet-build-semantic-mappings` | Mindnet: Build semantic mapping blocks (by section) | **editorCallback** – nur bei geöffneter MD-Datei | `semanticMappingBuilder.ts`: Mapping-Blöcke pro Sektion bauen/überarbeiten |
|
||||
| `mindnet-fix-findings` | Mindnet: Fix Findings (Current Section) | **editorCallback** | `fixFindingsCommand.ts`: fehlende Links einfügen, Kandidaten-Edges bestätigen |
|
||||
|
||||
3. **"Mindnet: Insert Intra-Note Edge"**
|
||||
- Öffnet Edge-Type-Chooser mit Section-Type-Support
|
||||
- Erstellt Intra-Note-Link mit Block-Reference
|
||||
**Hinweis:** Commands mit `editorCallback` erscheinen in Obsidian nur, wenn eine Markdown-Datei im Editor aktiv ist. Ohne geöffnete Note sind sie in der Befehlspalette ausgegraut bzw. nicht ausführbar. Das kann dazu führen, dass Nutzer sie „nicht als Befehle abrufbar“ erleben, wenn sie z. B. in Einstellungen oder im Graph sind.
|
||||
|
||||
4. **"Mindnet: Validate Section Types"**
|
||||
- Führt Section-Type-Lint-Regeln aus
|
||||
- Zeigt Findings in Console
|
||||
**Offene / geplante Commands (Lastenheft):**
|
||||
1. **"Mindnet: Add Section Type"** (noch nicht umgesetzt)
|
||||
- Öffnet Section-Type-Modal, fügt `[!section]` Callout ein
|
||||
|
||||
2. **"Mindnet: Add Block ID to Heading"** (noch nicht umgesetzt)
|
||||
- Generiert Block-ID für aktuelles Heading, fügt `^block-id` hinzu
|
||||
|
||||
3. **"Mindnet: Insert Intra-Note Edge"** (noch nicht umgesetzt)
|
||||
- Edge-Type-Chooser mit Section-Type-Support, Intra-Note-Link mit Block-Reference
|
||||
|
||||
4. **"Mindnet: Validate Section Types"** (noch nicht umgesetzt)
|
||||
- Section-Type-Lint-Regeln ausführen, Findings in Console
|
||||
|
||||
**Neue Anforderung (FA-PL-12a): Sichtbarkeit der Edge-Commands**
|
||||
- Die Befehle **Edge-Type ändern** und **Build semantic mapping blocks** sollen für Nutzer klar auffindbar sein.
|
||||
- Option A: Beibehalten von `editorCallback` und in der Dokumentation (z. B. Benutzerhandbuch) festhalten: „Eine Markdown-Note muss geöffnet sein.“
|
||||
- Option B: Zusätzlich einen Command ohne Editor (z. B. `callback`) anbieten, der prüft, ob eine MD-Datei aktiv ist, und ggf. eine Notice ausgibt („Bitte zuerst eine Markdown-Datei öffnen“), damit der Befehl immer in der Palette erscheint.
|
||||
|
||||
**Kompatibilität:**
|
||||
- Bestehende Commands bleiben unverändert
|
||||
- Neue Commands sind zusätzlich
|
||||
- Neue/angepasste Commands sind zusätzlich oder klären die Nutzung
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Phase 6: Dictionary-Integration
|
||||
#### Edge-Überarbeitung in bestehenden Noten (bereits implementiert)
|
||||
|
||||
#### FA-PL-13: Types-Loader
|
||||
Die folgenden **Module** unterstützen das Überarbeiten und Verbinden von Edges in bestehenden Noten. Sie sind über die oben genannten Commands erreichbar (jeweils bei geöffneter Markdown-Datei):
|
||||
|
||||
| Modul | Funktion | Aufruf |
|
||||
|-------|----------|--------|
|
||||
| `src/mapping/edgeTypeSelector.ts` | Kantentyp für einen Link, mehrere Links oder ganze Note setzen; neuer Link mit Kantentyp | Command „Mindnet: Edge-Type ändern“ |
|
||||
| `src/mapping/semanticMappingBuilder.ts` | Pro Sektion Semantic-Mapping-Blöcke bauen/überarbeiten, Links mit Kantentypen versehen | Command „Mindnet: Build semantic mapping blocks (by section)“ |
|
||||
| `src/mapping/updateMappingBlocks.ts` | `[[rel:type\|link]]` in Content finden, in Mapping-Blöcke übernehmen | Wird vom Edge-Type-Selector nach Änderung aufgerufen |
|
||||
| `src/commands/fixFindingsCommand.ts` | Fehlende Links einfügen, Kandidaten-Edges in explizite Edges übernehmen | Command „Mindnet: Fix Findings (Current Section)“ |
|
||||
|
||||
Ohne geöffnete Markdown-Datei erscheinen „Edge-Type ändern“ und „Build semantic mapping blocks“ in der Befehlspalette ausgegraut (siehe FA-PL-12a).
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Erweiterung: Edge-Typen pro Sektion im Interview (implementiert, Jan 2026)
|
||||
|
||||
Die folgende Erweiterung wurde umgesetzt und hat Auswirkungen auf Parsing, Renderer und Mapping.
|
||||
|
||||
#### FA-PL-14: Section-Edges und Note-Edges im Interview
|
||||
|
||||
**Anforderung:** Im Interview-Wizard sollen Section-zu-Section- und Note-Edges (inkl. Abschnitts-Links `Note#Abschnitt`) konfigurierbar sein und im gerenderten Markdown korrekt erscheinen.
|
||||
|
||||
**Umsetzung (Kernkomponenten):**
|
||||
|
||||
1. **SectionEdgesOverviewModal** (`src/ui/SectionEdgesOverviewModal.ts`)
|
||||
- Zeigt am Ende des Interviews alle Section-Edges und Note-Edges.
|
||||
- Erlaubt Anpassung der Edge-Typen; Note-Ziele werden als vollständiger Link (z. B. `Note#Abschnitt`) gespeichert.
|
||||
- Liefert `sectionEdges` und `noteEdges` an den Wizard.
|
||||
|
||||
2. **LinkTargetPickerModal** (`src/ui/LinkTargetPickerModal.ts`)
|
||||
- Nach Auswahl einer Note (Entity Picker) kann gewählt werden: ganze Note oder konkreter Abschnitt (`Note#Abschnitt`).
|
||||
- Der gewählte Link wird in Inline-Micro-Edging und in den Übersichts-Dialog übernommen.
|
||||
|
||||
3. **Renderer** (`src/interview/renderer.ts`)
|
||||
- `RenderOptions.noteEdges`: Map von Block-ID → (toNote bzw. `Note#Abschnitt` → edgeType).
|
||||
- Note-Edges werden pro Sektion als `>> [!edge] type` und `>> [[Note#Abschnitt]]` im Semantic-Mapping-Block ausgegeben.
|
||||
- `buildAbstractWrapper` akzeptiert beliebige Wikilinks `[[…]]`, nicht nur `[[#^block-id]]`.
|
||||
|
||||
4. **Links mit Abschnitt erhalten**
|
||||
- **parseRelLinks.ts:** `linkBasename` behält `#Abschnitt`; Ersetzung erfolgt als `[[Note#Abschnitt]]`; Edge-Map verwendet vollen Link als Key.
|
||||
- **sectionParser.ts (extractWikilinks):** Links behalten `#Abschnitt` (kein Strip mehr); `section.links` enthält z. B. `Note#Abschnitt`.
|
||||
- **semanticMappingBuilder.ts / updateMappingBlocks.ts:** Beim Mergen wird der passende Section-Link (mit `#`) als Key verwendet; Mapping-Blöcke schreiben `>> [[Note#Abschnitt]]`.
|
||||
|
||||
**Auswirkungen:**
|
||||
- Bestehende Notes ohne `#Abschnitt` bleiben unverändert.
|
||||
- Chain Workbench, Graph-Index und Backend können `Note#Abschnitt`-Links nutzen, sofern sie diese Form unterstützen.
|
||||
- Lint/Validierung: optional prüfen, dass Abschnitts-Links konsistent sind.
|
||||
|
||||
**Dokumentation:** Siehe auch `docs/WP26_Plugin_Interface_Specification.md` (Abschnitt Interview, Edge-per-Section, Note#Abschnitt).
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Phase 6: Dictionary-Integration
|
||||
|
||||
#### FA-PL-13: Types-Loader (wie bisher)
|
||||
|
||||
**Anforderung:** Laden und Validierung von `types.yaml`.
|
||||
|
||||
|
|
@ -851,16 +940,31 @@ export interface WP26FeatureFlags {
|
|||
- FA-PL-11: Schema-Validierung für Intra-Note-Edges
|
||||
|
||||
### Phase 5: Commands
|
||||
- FA-PL-12: Neue Commands
|
||||
- FA-PL-12: Commands für WP-26 und Edge-Überarbeitung (Stand: implementierte vs. geplante Commands)
|
||||
- FA-PL-12a: Sichtbarkeit Edge-Commands (optional: ohne Editor-Kontext auffindbar machen)
|
||||
|
||||
### Phase 6: Dictionary-Integration
|
||||
- FA-PL-13: Types-Loader
|
||||
|
||||
### Erweiterung (implementiert)
|
||||
- FA-PL-14: Section-Edges und Note-Edges im Interview (inkl. Note#Abschnitt)
|
||||
|
||||
---
|
||||
|
||||
## 8. Risiken und Mitigation
|
||||
## 8. Nächste Schritte (Empfehlung)
|
||||
|
||||
### 8.1 Risiken
|
||||
1. **Commands auffindbar machen:** FA-PL-12a umsetzen (Dokumentation und/oder zusätzlichen Command ohne editorCallback), damit „Edge-Type ändern“ und „Build semantic mapping blocks“ in der Befehlspalette zuverlässig gefunden werden.
|
||||
2. **Kausal-Funktionen sichtbar machen:** „Inspect Chains“ und „Chain Workbench“ nutzen editorCallback und erscheinen nur bei geöffneter MD-Datei (siehe Abschnitt 2.1.4). Optionen: (a) Im Benutzerhandbuch festhalten: „Eine Markdown-Note muss geöffnet sein.“ (b) Zusätzlich Commands ohne editorCallback anbieten, die bei fehlender Datei einen Hinweis anzeigen, damit die Befehle in der Palette sichtbar bleiben.
|
||||
3. **Hinweis bei fehlenden Chain-Dictionaries:** Wenn `chain_roles.yaml` oder `chain_templates.yaml` nicht geladen werden können, soll vor Ausführung von Inspect Chains / Chain Workbench ein **Notice** erscheinen (z. B. „Chain Roles nicht geladen. Bitte Pfad in den Einstellungen prüfen.“), damit Nutzer nicht mit leerer oder reduzierter Analyse dastehen.
|
||||
4. **Geplante Commands (FA-PL-12):** Priorisierung von „Add Section Type“, „Add Block ID to Heading“, „Insert Intra-Note Edge“, „Validate Section Types“ gemäß Nutzerbedarf.
|
||||
5. **Interview-Edge-Erweiterung (FA-PL-14):** Abnahme mit Backend (Note#Abschnitt in Links/Mapping-Blöcken); ggf. Lint-Regel für konsistente Abschnitts-Links.
|
||||
6. **WP26-Spezifikation:** Implementierungsstand in `WP26_Plugin_Interface_Specification.md` fortlaufend anpassen (siehe dort Abschnitt 9 / 10).
|
||||
|
||||
---
|
||||
|
||||
## 9. Risiken und Mitigation
|
||||
|
||||
### 9.1 Risiken
|
||||
|
||||
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|
||||
|--------|-------------------|--------|------------|
|
||||
|
|
@ -871,9 +975,9 @@ export interface WP26FeatureFlags {
|
|||
|
||||
---
|
||||
|
||||
## 9. Erfolgskriterien
|
||||
## 10. Erfolgskriterien
|
||||
|
||||
### 9.1 Funktionale Kriterien
|
||||
### 10.1 Funktionale Kriterien
|
||||
|
||||
- ✅ Alle bestehenden Funktionen funktionieren unverändert
|
||||
- ✅ Section-Types werden korrekt erkannt und geparst
|
||||
|
|
@ -882,7 +986,7 @@ export interface WP26FeatureFlags {
|
|||
- ✅ UI-Komponenten unterstützen Section-Types
|
||||
- ✅ Lint-Regeln validieren Section-Types
|
||||
|
||||
### 9.2 Qualitätskriterien
|
||||
### 10.2 Qualitätskriterien
|
||||
|
||||
- ✅ Keine Breaking Changes
|
||||
- ✅ Vollständige Abwärtskompatibilität
|
||||
|
|
@ -891,18 +995,23 @@ export interface WP26FeatureFlags {
|
|||
|
||||
---
|
||||
|
||||
## 10. Anhänge
|
||||
## 11. Anhänge
|
||||
|
||||
### 10.1 Referenzen
|
||||
### 11.1 Referenzen
|
||||
|
||||
- Backend Lastenheft: `docs/06_Roadmap/06_LH_Section_Types_Intra_Note_Edges.md` (v1.3)
|
||||
- Backend Plugin Interface: `docs/WP26_Plugin_Interface_Specification.md`
|
||||
- Backend Implementation Checklist: `docs/WP26_Implementation_Checklist.md`
|
||||
|
||||
### 10.2 Glossar
|
||||
### 11.2 Glossar
|
||||
|
||||
Siehe Backend Lastenheft Kapitel 1.3.
|
||||
|
||||
### 11.3 Chain Inspector & Chain Workbench (detailliert)
|
||||
|
||||
- **Lastenheft Chain Inspector & Chain Workbench:** `docs/09_Chain_Inspector_Workbench_Lastenheft.md` – vollständige fachliche und technische Anforderungen (effective_type, Findings, Todos, Commands).
|
||||
- **Umsetzungsplan Chain Inspector & Chain Workbench:** `docs/09_Chain_Inspector_Workbench_Umsetzungsplan.md` – detaillierte Phasen, Tasks und offene Punkte aus früheren Implementierungsplänen (Section-Type-Integration, Findings-Integration, FA-PL-08).
|
||||
|
||||
---
|
||||
|
||||
**Ende des Lastenhefts**
|
||||
|
|
|
|||
250
docs/09_Chain_Inspector_Workbench_Lastenheft.md
Normal file
250
docs/09_Chain_Inspector_Workbench_Lastenheft.md
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
# LASTENHEFT: Chain Inspector & Chain Workbench
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 28. Januar 2026
|
||||
**Status:** Entwurf
|
||||
**Projekt:** mindnet_obsidian (Obsidian Community Plugin)
|
||||
**Bezug:** WP-26 Integration (06_LH_WP26_Plugin_Integration.md), FA-PL-08; Chain Inspector v0.4.x / v0.4.2; Chain Workbench Findings Integration (10_Workbench_Findings_Integration.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Einleitung
|
||||
|
||||
### 1.1 Zweck
|
||||
|
||||
Dieses Lastenheft definiert die **vollständigen fachlichen und technischen Anforderungen** für die Kausal-Funktionen **Chain Inspector** und **Chain Workbench** im mindnet_obsidian Plugin. Es umfasst:
|
||||
|
||||
- Die Integration der **Section-Type-Logik** (effective_type) in Chain-Analyse und Workbench (FA-PL-08).
|
||||
- Die Anforderungen aus den früheren Implementierungsberichten (Chain Inspector v0.0–v0.4.2, Chain Workbench 0.5.x).
|
||||
- Offene Punkte aus dem Vorschlag **Workbench Findings Integration** (Findings → Todos, Actions).
|
||||
- Commands, Sichtbarkeit, Dictionary-Abhängigkeiten und UX.
|
||||
|
||||
### 1.2 Geltungsbereich
|
||||
|
||||
- **Chain Inspector:** Analyse kausaler Ketten um die aktuelle Section (Template-Matching, Findings, Rollen, Gaps).
|
||||
- **Chain Workbench:** UI für Findings, Template-Matches, TODOs und Fix-Actions.
|
||||
- **Section-Types:** Explizite Typen pro Abschnitt (`[!section] type`), Fallback auf Note-Type; Verwendung als `effective_type` in Chain-Matching.
|
||||
|
||||
### 1.3 Abhängigkeiten
|
||||
|
||||
- **Backend:** WP-26 (Section Types & Intra-Note-Edges).
|
||||
- **Plugin:** Section-Type-Callout-Parsing (FA-PL-01), Block-ID-Extraktion (FA-PL-02), Types-Loader (FA-PL-13).
|
||||
- **Dictionary:** `chain_roles.yaml`, `chain_templates.yaml`, `edge_vocabulary.md`, optional `types.yaml`, `graph_schema.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Begriffe und Definitionen
|
||||
|
||||
| Begriff | Definition |
|
||||
|--------|------------|
|
||||
| **Effective Type** | Für einen Knoten (Section/Note): `sectionType \|\| noteType`. Wird für Template-Slot-Matching und Gap-Detection verwendet. |
|
||||
| **Section-Type** | Explizit per `> [!section] type` definierter Typ eines Abschnitts. |
|
||||
| **Note-Type** | Aus Frontmatter `type:` der Note. |
|
||||
| **Template Match** | Zuweisung von Candidate-Nodes zu Template-Slots unter Berücksichtigung von `allowed_node_types` und `allowed_edge_roles`. |
|
||||
| **Finding** | Ein vom Chain Inspector erzeugtes Analyseergebnis (z. B. `no_causal_roles`, `missing_link`, `dangling_target`). |
|
||||
| **Workbench Todo** | Aus Finding oder Template-Match abgeleitete konkrete Aufgabe mit Priorität und optionalen Actions. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Chain Inspector – Anforderungen
|
||||
|
||||
### 3.1 Kernfunktion
|
||||
|
||||
- **Eingabe:** Aktuelle Datei + aktuelle Section (Cursor-Position / Heading).
|
||||
- **Ausgabe:** `ChainInspectorReport` mit Nachbarn, Pfaden, Findings, Template-Matches, Analyse-Meta, Templates-Source, optional Template-Matching-Profile.
|
||||
|
||||
### 3.2 Effective Type (Section-Type-Integration)
|
||||
|
||||
**LH-CI-01: Effective Type pro Knoten**
|
||||
|
||||
- Für jeden **Knoten** (file + heading) muss ein **effective_type** ermittelt werden.
|
||||
- **Regel:** `effectiveType = sectionType || noteType`.
|
||||
- **sectionType:** Aus geparster Note: Section, die durch `heading` identifiziert wird, enthält ein `[!section] type` Callout → dieser Typ.
|
||||
- **noteType:** Aus Frontmatter `type:` der zugehörigen Datei.
|
||||
- **Fallback:** Wenn weder Section-Type noch Note-Type vorhanden: `"unknown"`.
|
||||
|
||||
**Betroffene Stellen:**
|
||||
|
||||
- `src/analysis/templateMatching.ts`: Aktuell wird nur `extractNoteType(content)` pro Datei verwendet; für Section-Knoten (mit heading) muss pro Section der Section-Type aus dem geparsten Dokument gelesen werden.
|
||||
- `src/analysis/chainInspector.ts`: Report und Findings müssen effective_type unterstützen (z. B. in SlotAssignments oder Meta).
|
||||
- `src/mapping/sectionParser.ts`: Muss `[!section]` erkennen und `sectionType` in `NoteSection` liefern (vgl. FA-PL-01); aktuell fehlt `sectionType` in `NoteSection`.
|
||||
|
||||
### 3.3 Template-Matching mit Section-Types
|
||||
|
||||
**LH-CI-02: Slot-Matching mit effective_type**
|
||||
|
||||
- Beim Zuordnen von Candidate-Nodes zu Template-Slots gilt: **Slot-Constraint** `allowed_node_types` wird gegen **effective_type** des Knotens geprüft (nicht nur gegen noteType).
|
||||
- Bestehende Logik in `nodeMatchesSlot(node, slot)` muss mit `node.effectiveType` (oder äquivalent) arbeiten.
|
||||
|
||||
**LH-CI-03: Provenance**
|
||||
|
||||
- Im Report soll erkennbar sein, ob ein Slot-Type aus Section-Type oder Note-Type stammt (optional: `typeSource: "section" | "note" | "unknown"` pro Slot-Zuordnung).
|
||||
|
||||
### 3.4 Findings
|
||||
|
||||
**LH-CI-04: Bestehende Finding-Codes**
|
||||
|
||||
- Folgende Finding-Codes müssen unterstützt und dokumentiert sein:
|
||||
- `dangling_target`, `dangling_target_heading`
|
||||
- `only_candidates`, `missing_edges`, `no_causal_roles`, `one_sided_connectivity`
|
||||
- `missing_slot_<slotId>`, `weak_chain_roles`
|
||||
- Optional: `slot_type_mismatch` (v0.4 Report: bisher nicht implementiert, da Matching Mismatches verhindert).
|
||||
|
||||
**LH-CI-05: Findings mit Section-Type-Kontext**
|
||||
|
||||
- Findings können optional `effectiveType` der betroffenen Section bzw. des Knotens enthalten (für bessere Workbench-Todos und Fix-Actions).
|
||||
|
||||
### 3.5 Gap-Detection und Section-Types
|
||||
|
||||
**LH-CI-06: Fehlende Section-Types als Gap**
|
||||
|
||||
- Wenn ein Template einen Slot mit z. B. `allowed_node_types: ["experience"]` vorsieht und die aktuelle Section keinen Section-Type hat, aber der Note-Type z. B. "concept" ist, soll dies als Gap/Finding behandelbar sein (z. B. „Section-Type setzen oder Note-Type anpassen“).
|
||||
- Konkret: Gap-Detection soll erkennen, wenn **effective_type** nicht zu den erlaubten Slot-Types passt, und ggf. ein Finding/Todo vorschlagen (z. B. „Add Section Type experience“).
|
||||
|
||||
### 3.6 Dictionary-Abhängigkeiten
|
||||
|
||||
**LH-CI-07: Verhalten bei fehlenden Dictionaries**
|
||||
|
||||
- Wenn `chain_roles.yaml` oder `chain_templates.yaml` nicht geladen werden können:
|
||||
- Es soll eine **Notice** angezeigt werden (z. B. „Chain Roles nicht geladen. Bitte Pfad in den Einstellungen prüfen.“) und der Command **abbrechen** (kein stilles Weiterlaufen mit reduzierter Funktionalität).
|
||||
- Implementierungsstand: In `main.ts` wurde bereits eine Prüfung und Notice eingebaut; im Lastenheft wird dies als verbindlich festgehalten.
|
||||
|
||||
### 3.7 Commands und Sichtbarkeit
|
||||
|
||||
**LH-CI-08: Command „Mindnet: Inspect Chains (Current Section)“**
|
||||
|
||||
- **ID:** `mindnet-inspect-chains`.
|
||||
- **Kontext:** editorCallback (nur bei geöffneter Markdown-Datei).
|
||||
- **Verhalten:** Führt Chain Inspector aus, gibt Report in Console aus (oder vergleichbar).
|
||||
- **Optional (FA-PL-12a):** Zusätzlicher Command ohne editorCallback, der bei fehlender Datei eine Notice anzeigt, damit der Befehl in der Palette sichtbar bleibt.
|
||||
|
||||
---
|
||||
|
||||
## 4. Chain Workbench – Anforderungen
|
||||
|
||||
### 4.1 Kernfunktion
|
||||
|
||||
- **Eingabe:** Wie Chain Inspector (aktuelle Datei + Section) plus geladene Chain-Templates, Chain-Roles, Edge-Vocabulary.
|
||||
- **Ausgabe:** Workbench-Model mit **Matches** (Template-Matches inkl. Status/Todos) und **globalTodos** (aus Findings).
|
||||
|
||||
### 4.2 Section-Type in Workbench-Model
|
||||
|
||||
**LH-CW-01: WorkbenchMatch mit Section-Types**
|
||||
|
||||
- Jeder Match soll optional **effective_type** (oder Section-Type) pro zugewiesenem Slot führen (für Anzeige und für Todo-Vorschläge).
|
||||
- Vgl. WP26-Spezifikation: `sectionTypes?: Map<string, string>`.
|
||||
|
||||
**LH-CW-02: Todos mit Section-Type-Kontext**
|
||||
|
||||
- Missing-Link- und Missing-Slot-Todos sollen **fromSectionType** / **toSectionType** (bzw. effective_type) enthalten, damit Edge-Type-Vorschläge aus `graph_schema.md` korrekt berechnet werden können.
|
||||
|
||||
### 4.3 Findings → Todos (Workbench Findings Integration)
|
||||
|
||||
**LH-CW-03: Generierung von Todos aus Findings**
|
||||
|
||||
- Die in **10_Workbench_Findings_Integration.md** beschriebene Funktion **generateFindingsTodos()** soll umgesetzt werden:
|
||||
- Eingabe: `Finding[]`, `IndexedEdge[]`, Kontext `{ file, heading }`.
|
||||
- Ausgabe: `WorkbenchTodoUnion[]` (u. a. `DanglingTargetTodo`, `OnlyCandidatesTodo`, `NoCausalRolesTodo`, `MissingEdgesTodo`, `OneSidedConnectivityTodo`).
|
||||
- **Todo-Typen** sind in `src/workbench/types.ts` bereits definiert (`dangling_target`, `only_candidates`, `no_causal_roles`, etc.); die **Generierung** aus Findings und die **Integration** in `buildWorkbenchModel()` sind umzusetzen.
|
||||
|
||||
**LH-CW-04: Integration in buildWorkbenchModel**
|
||||
|
||||
- `buildWorkbenchModel()` soll neben Template-Match-basierten Todos auch **globalTodos** aus `report.findings` erzeugen (über `generateFindingsTodos()`).
|
||||
- **WorkbenchModel** soll `globalTodos?: WorkbenchTodoUnion[]` enthalten.
|
||||
|
||||
**LH-CW-05: UI-Anzeige**
|
||||
|
||||
- Im Chain Workbench Modal sollen **Section-Level Issues** (aus Findings) getrennt von **Template-Match-Todos** angezeigt werden (z. B. zuerst „Section-Level Issues“, dann „Template Matches“).
|
||||
|
||||
### 4.4 TODO-Generator und Section-Types
|
||||
|
||||
**LH-CW-06: Section-Type-Vorschläge**
|
||||
|
||||
- Der TODO-Generator soll bei fehlendem oder unpassendem Section-Type Vorschläge liefern (z. B. „Add Section Type: experience“), vgl. FA-PL-08.
|
||||
|
||||
### 4.5 Fix-Actions
|
||||
|
||||
**LH-CW-07: Bestehende Fix-Actions**
|
||||
|
||||
- Die in Chain Inspector v0.3 und im Fix-Findings-Command implementierten Actions bleiben erhalten und erweiterbar:
|
||||
- `dangling_target`: Create Missing Note, Retarget Link
|
||||
- `dangling_target_heading`: Create Missing Heading, Retarget
|
||||
- `only_candidates`: Promote Candidates, Create Explicit Edges
|
||||
- **Optional:** Actions für `no_causal_roles` (z. B. „Edge-Type ändern“) und `missing_edges` („Add edges to section“) wie in 10_Workbench_Findings_Integration.md skizziert.
|
||||
|
||||
### 4.6 Command und Sichtbarkeit
|
||||
|
||||
**LH-CW-08: Command „Mindnet: Chain Workbench (Current Section)“**
|
||||
|
||||
- **ID:** `mindnet-chain-workbench`.
|
||||
- **Kontext:** editorCallback.
|
||||
- **Verhalten:** Öffnet Chain Workbench Modal mit aktuellem Report/Model.
|
||||
- **Optional (FA-PL-12a):** Zusätzlicher Command ohne Editor, mit Notice bei fehlender Datei.
|
||||
|
||||
### 4.7 Scan Chain Gaps
|
||||
|
||||
**LH-CW-09: Command „Mindnet: Scan Vault for Chain Gaps“**
|
||||
|
||||
- Soll ebenfalls Notice auslösen, wenn Chain-Dictionaries fehlen; Section-Type-Logik soll bei Vault-Scan berücksichtigt werden (effective_type pro Section), sofern technisch eingebaut.
|
||||
|
||||
---
|
||||
|
||||
## 5. Abhängigkeiten von Parser und Mapping
|
||||
|
||||
### 5.1 Section-Parser (FA-PL-01)
|
||||
|
||||
- **NoteSection** muss erweitert werden um:
|
||||
- `sectionType: string | null` (aus `[!section] type`).
|
||||
- `blockId: string | null` (aus Heading `^block-id`).
|
||||
- Ohne diese Erweiterung kann Chain Inspector **keinen** Section-Type pro Section nutzen; dann bleibt nur Note-Type (bisheriges Verhalten).
|
||||
|
||||
### 5.2 Graph-Index
|
||||
|
||||
- **buildNoteIndex** nutzt `splitIntoSections`; sobald `NoteSection.sectionType` existiert, muss der Graph-Index oder eine nachgelagerte Schicht (z. B. templateMatching) pro Section den Section-Type aus den geparsten Sections auslesen und an Candidate-Nodes weitergeben.
|
||||
|
||||
### 5.3 Types-Loader (FA-PL-13)
|
||||
|
||||
- Optional: Validierung von Section-Types gegen `types.yaml` (z. B. Lint oder Hinweis im Workbench).
|
||||
|
||||
---
|
||||
|
||||
## 6. Nichtfunktionale Anforderungen
|
||||
|
||||
### 6.1 Performance
|
||||
|
||||
- Template-Matching: Max. 30 Candidate-Nodes, Backtracking für typisch ≤ 5 Slots; Top-K Matches begrenzt (z. B. 3).
|
||||
- Keine Blockierung der UI; bei großen Reports ggf. asynchron oder mit Fortschrittsanzeige.
|
||||
|
||||
### 6.2 Abwärtskompatibilität
|
||||
|
||||
- Notes **ohne** Section-Types funktionieren unverändert: effective_type = noteType.
|
||||
- Bestehende Chain-Templates und Chain-Roles bleiben gültig.
|
||||
|
||||
### 6.3 Dokumentation
|
||||
|
||||
- Benutzerhandbuch: Erklärung, dass „Inspect Chains“ und „Chain Workbench“ nur bei **geöffneter Markdown-Datei** in der Befehlspalette verfügbar sind.
|
||||
- Dokumentation der Finding-Codes und Todo-Typen (z. B. in diesem Lastenheft oder im Konzeptdokument 03_chain_identification_and_matching.md).
|
||||
|
||||
---
|
||||
|
||||
## 7. Erfolgskriterien (Zusammenfassung)
|
||||
|
||||
- **Chain Inspector:** Verwendet effective_type (Section-Type oder Note-Type) für Template-Matching und Gap-Detection; Findings inkl. Section-Type-Kontext wo sinnvoll; Notice bei fehlenden Dictionaries.
|
||||
- **Chain Workbench:** Zeigt Template-Matches und Section-Level-Todos (aus Findings); Todos enthalten Section-Type-Information für Edge-Vorschläge; Fix-Actions für die definierten Finding-Typen.
|
||||
- **Section-Type-Integration:** Section-Parser liefert sectionType; Template-Matching und Workbench nutzen effective_type durchgängig.
|
||||
- **Offene Punkte aus Findings-Integration:** generateFindingsTodos umgesetzt, in buildWorkbenchModel integriert, UI für globalTodos; Priorisierung/Deduplizierung wie in Umsetzungsplan geregelt.
|
||||
|
||||
---
|
||||
|
||||
## 8. Referenzen
|
||||
|
||||
- **06_LH_WP26_Plugin_Integration.md** – FA-PL-08, FA-PL-12, FA-PL-12a, FA-PL-14
|
||||
- **WP26_Plugin_Interface_Specification.md** – Abschnitte 3.4, 8.2
|
||||
- **10_Workbench_Findings_Integration.md** – Findings → Todos, Todo-Typen, Phasen
|
||||
- **CHAIN_INSPECTOR_V04_REPORT.md**, **CHAIN_INSPECTOR_V042_REPORT.md** – Template Matching, Profiles
|
||||
- **02_concepts/03_chain_identification_and_matching.md** – Konzepte
|
||||
- **08_Testing_Chain_Workbench.md** – Testanleitung
|
||||
|
||||
---
|
||||
|
||||
**Ende des Lastenhefts**
|
||||
199
docs/09_Chain_Inspector_Workbench_Umsetzungsplan.md
Normal file
199
docs/09_Chain_Inspector_Workbench_Umsetzungsplan.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Umsetzungsplan: Chain Inspector & Chain Workbench (Section-Type-Integration)
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 28. Januar 2026
|
||||
**Status:** Entwurf
|
||||
**Bezug:** 09_Chain_Inspector_Workbench_Lastenheft.md, 06_LH_WP26_Plugin_Integration.md (FA-PL-08), 10_Workbench_Findings_Integration.md, CHAIN_INSPECTOR_V0x_REPORTs
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht
|
||||
|
||||
Dieser Umsetzungsplan beschreibt die **detaillierte Implementierungsreihenfolge** für:
|
||||
|
||||
1. **Section-Type-Integration** in Chain Inspector und Chain Workbench (FA-PL-08, Lastenheft 09).
|
||||
2. **Offene Punkte** aus früheren Implementierungsplänen (Findings Integration, Chain Inspector Reports, Lastenheft „Nächste Schritte“).
|
||||
|
||||
Die Phasen bauen aufeinander auf; Abhängigkeiten sind explizit genannt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Offene Punkte aus früheren Plänen (Konsolidiert)
|
||||
|
||||
### 2.1 Aus 10_Workbench_Findings_Integration.md
|
||||
|
||||
| Offener Punkt | Beschreibung | Integration in Umsetzungsplan |
|
||||
|---------------|--------------|-------------------------------|
|
||||
| **Findings → Todos** | Workbench nutzt aktuell nur Template-Matches, nicht Findings (dangling_target, no_causal_roles, etc.). | Phase 4: generateFindingsTodos, buildWorkbenchModel, UI. |
|
||||
| **Todo-Typen** | DanglingTargetTodo, OnlyCandidatesTodo, NoCausalRolesTodo, MissingEdgesTodo, OneSidedConnectivityTodo. | Types bereits in workbench/types.ts; Generierung in Phase 4. |
|
||||
| **Priorität Findings vs. Template** | Sollen Findings-Todos höhere Priorität haben? | Entscheidung: Section-Level Issues zuerst anzeigen (LH-CW-05). |
|
||||
| **Duplikate** | Vermeidung von Duplikaten (z. B. dangling_target vs. missing_link). | Phase 4: Deduplizierung nach Kontext (file, heading, finding.code). |
|
||||
| **Filterung** | Filter nach Severity (z. B. nur Errors). | Optional: Phase 5 oder später. |
|
||||
| **Actions** | create_missing_note, retarget_link, promote_all_candidates, change_edge_type, etc. | Teilweise in Fix-Findings-Command (v0.3); no_causal_roles/missing_edges optional Phase 5. |
|
||||
|
||||
### 2.2 Aus CHAIN_INSPECTOR Reports (v0.4, v0.4.2)
|
||||
|
||||
| Offener Punkt | Beschreibung | Integration |
|
||||
|---------------|--------------|-------------|
|
||||
| **Top-K Limit** | Nur Top-1 Match pro Template (K=1). | Optional: K konfigurierbar (z. B. 3) – bereits maxTemplateMatches in Options. |
|
||||
| **Node-Limit** | Max 30 Candidate-Nodes. | Unverändert; Sicherheit. |
|
||||
| **slot_type_mismatch** | Finding nicht implementiert. | Optional: Phase 3 oder später; aktuell Matching verhindert Mismatches. |
|
||||
| **Node-Type = Note-Type** | Template-Matching nutzt nur Frontmatter type. | Phase 1–2: effective_type (Section-Type \|\| Note-Type). |
|
||||
| **Profile (discovery/decisioning)** | Bereits in v0.4.2. | Unverändert; Section-Type-Integration ergänzt. |
|
||||
|
||||
### 2.3 Aus 06_LH_WP26_Plugin_Integration.md (Nächste Schritte)
|
||||
|
||||
| Offener Punkt | Beschreibung | Integration |
|
||||
|---------------|--------------|-------------|
|
||||
| **Commands sichtbar** | Inspect Chains / Chain Workbench nur bei geöffneter MD-Datei. | Optional: Phase 5 – zusätzliche Commands ohne editorCallback mit Notice. |
|
||||
| **Notice bei fehlenden Dictionaries** | Chain Roles/Templates nicht geladen → Notice + Abbruch. | Bereits umgesetzt in main.ts; in Lastenheft 09 verbindlich (LH-CI-07). |
|
||||
| **FA-PL-08** | Chain Workbench mit Section-Types. | Phase 1–3: effective_type durchgängig. |
|
||||
|
||||
### 2.4 Aus WP26_Plugin_Interface_Specification.md
|
||||
|
||||
| Offener Punkt | Beschreibung | Integration |
|
||||
|---------------|--------------|-------------|
|
||||
| **Section-Type in Chain-Matching** | WorkbenchMatch.sectionTypes, MissingLinkTodo.fromSectionType/toSectionType. | Phase 2 (Template-Matching), Phase 4 (Todos). |
|
||||
| **Intra-Note-Edge-Erkennung** | detectIntraNoteEdges für bessere Chain-Analyse. | Optional: Phase 3 oder später; abhängig von FA-PL-03/Graph-Index. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Voraussetzungen (Vorbedingungen)
|
||||
|
||||
- **FA-PL-01 (Section-Type-Callout-Parsing):** `[!section]` muss im Section-Parser erkannt und als `sectionType` in `NoteSection` gespeichert werden.
|
||||
- **Aktueller Stand:** `NoteSection` in `sectionParser.ts` hat **kein** Feld `sectionType` bzw. `blockId`. Diese Erweiterung ist **Voraussetzung** für effective_type in Chain Inspector.
|
||||
- **FA-PL-02 (Block-ID):** Optional für bessere Referenzierung; nicht zwingend für effective_type.
|
||||
- **FA-PL-13 (Types-Loader):** Optional für Validierung von Section-Types.
|
||||
|
||||
Ohne FA-PL-01 (Section-Parser mit sectionType) kann Phase 1 nur mit **Note-Type** (bisherig) arbeiten; effective_type wird dann erst nach Implementierung von FA-PL-01 wirksam.
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 1: Section-Parser und effective_type-Grundlage
|
||||
|
||||
**Ziel:** Section-Type in der Codebasis verfügbar machen und in Template-Matching als effective_type nutzen.
|
||||
|
||||
### 4.1 Tasks
|
||||
|
||||
| Nr | Task | Beschreibung | Dateien | Abhängigkeit |
|
||||
|----|------|--------------|---------|---------------|
|
||||
| 1.1 | **NoteSection um sectionType und blockId erweitern** | Interface `NoteSection` um `sectionType: string \| null` und `blockId: string \| null` erweitern; in `splitIntoSections` befüllen. | `src/mapping/sectionParser.ts` | – |
|
||||
| 1.2 | **Parsing von [!section] Callout** | In `splitIntoSections` pro Section nach Zeile mit `> [!section] <type>` suchen (Regex z. B. `/^\s*>\s*\[!section\]\s*(\w+)/i`), Typ extrahieren und in `sectionType` speichern. | `src/mapping/sectionParser.ts` | 1.1 |
|
||||
| 1.3 | **Parsing von Block-ID in Headings** | In Headings Muster `^#{1,6}\s+.+\s+\^([a-zA-Z0-9_-]+)$` erkennen, Block-ID in `blockId` speichern. | `src/mapping/sectionParser.ts` | 1.1 |
|
||||
| 1.4 | **Graph-Index: Sections mit sectionType bereitstellen** | `buildNoteIndex` oder eine neue Hilfsfunktion muss Sections inkl. sectionType an Aufrufer liefern (z. B. als Map `file:heading → sectionType` oder erweiterte SectionNode-Struktur). | `src/analysis/graphIndex.ts`, ggf. `chainInspector.ts` | 1.2 |
|
||||
| 1.5 | **effective_type-Funktion** | Hilfsfunktion `getEffectiveType(file, heading, sectionsWithType, noteTypeByFile): string` – liefert sectionType für (file, heading) falls vorhanden, sonst noteType, sonst "unknown". | `src/analysis/templateMatching.ts` oder neues Modul | 1.4 |
|
||||
|
||||
**Ergebnis Phase 1:** Section-Parser liefert sectionType; effective_type ist pro (file, heading) berechenbar.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 2: Chain Inspector – effective_type im Template-Matching
|
||||
|
||||
**Ziel:** Template-Matching und Findings nutzen effective_type statt nur noteType.
|
||||
|
||||
### 5.1 Tasks
|
||||
|
||||
| Nr | Task | Beschreibung | Dateien | Abhängigkeit |
|
||||
|----|------|--------------|---------|---------------|
|
||||
| 2.1 | **CandidateNode um effectiveType erweitern** | Interface `CandidateNode` um `effectiveType: string` erweitern (oder `noteType` durch effective_type ersetzen, falls Abwärtskompatibilität gewahrt wird: zusätzliches Feld). | `src/analysis/templateMatching.ts` | Phase 1 |
|
||||
| 2.2 | **buildCandidateNodes: effective_type setzen** | Beim Aufbau der Candidate-Nodes für jeden Knoten (file, heading) effective_type ermitteln: Section-Type aus geparsten Sections (file muss gelesen, in Sections zerlegt werden; sectionType für passende Section verwenden), sonst noteType aus Frontmatter. | `src/analysis/templateMatching.ts` | 1.5, 2.1 |
|
||||
| 2.3 | **nodeMatchesSlot mit effectiveType** | Slot-Constraint `allowed_node_types` gegen `node.effectiveType` prüfen (bereits konzeptionell; sicherstellen, dass das genutzte Feld effectiveType ist). | `src/analysis/templateMatching.ts` | 2.2 |
|
||||
| 2.4 | **TemplateMatch / Report: typeSource optional** | In SlotAssignments optional `typeSource?: "section" \| "note" \| "unknown"` pro Slot für Provenance. | `src/analysis/chainInspector.ts`, `templateMatching.ts` | 2.2 |
|
||||
| 2.5 | **Gap-Detection: fehlender Section-Type** | Wenn Slot z. B. "experience" erwartet, aber effective_type "unknown" oder nicht in allowed_node_types: Finding oder Todo-Vorschlag „Add Section Type“ / „Set section type to experience“. | `src/analysis/chainInspector.ts`, ggf. `todoGenerator.ts` | 2.3 |
|
||||
|
||||
**Ergebnis Phase 2:** Chain Inspector verwendet effective_type durchgängig; Gap-Detection kann fehlende Section-Types melden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 3: Chain Workbench – Section-Type in Model und Todos
|
||||
|
||||
**Ziel:** Workbench-Model und Todo-Typen enthalten Section-Type-Information; Edge-Vorschläge können section-type-basiert erfolgen.
|
||||
|
||||
### 6.1 Tasks
|
||||
|
||||
| Nr | Task | Beschreibung | Dateien | Abhängigkeit |
|
||||
|----|------|--------------|---------|---------------|
|
||||
| 3.1 | **WorkbenchMatch: sectionTypes / effectiveType pro Slot** | WorkbenchMatch (oder TemplateMatch) um optionale Map Slot-ID → effective_type erweitern; beim Aufbau aus TemplateMatch befüllen. | `src/workbench/types.ts`, `workbenchBuilder.ts` | Phase 2 |
|
||||
| 3.2 | **MissingLinkTodo / MissingSlotTodo: fromSectionType, toSectionType** | Todos um optionale Felder `fromSectionType`, `toSectionType` (bzw. effective_type) erweitern; in todoGenerator aus SlotAssignments/effective_type füllen. | `src/workbench/types.ts`, `todoGenerator.ts` | 2.4, 3.1 |
|
||||
| 3.3 | **Edge-Type-Vorschläge aus graph_schema** | Wenn fromSectionType und toSectionType vorhanden: `getTopologyInfo(sourceType, targetType)` aus Schema aufrufen und suggestedEdgeTypes daraus ableiten. | `src/workbench/todoGenerator.ts`, `schemaHelper.ts` | 3.2 |
|
||||
| 3.4 | **Section-Type-Todo „Add Section Type“** | Neuer Todo-Typ oder Erweiterung: Vorschlag „Add Section Type: experience“ bei Gap (vgl. LH-CW-06). | `src/workbench/todoGenerator.ts`, `types.ts` | 2.5, 3.1 |
|
||||
|
||||
**Ergebnis Phase 3:** Workbench zeigt Section-Type-Information in Matches und Todos; Edge-Vorschläge nutzen Section-Types wo vorhanden.
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase 4: Findings-Integration (Workbench Findings Integration)
|
||||
|
||||
**Ziel:** Findings aus Chain Inspector werden in Workbench als globalTodos angezeigt; keine Duplikate mit Template-Todos.
|
||||
|
||||
### 7.1 Tasks
|
||||
|
||||
| Nr | Task | Beschreibung | Dateien | Abhängigkeit |
|
||||
|----|------|--------------|---------|---------------|
|
||||
| 4.1 | **generateFindingsTodos implementieren** | Funktion wie in 10_Workbench_Findings_Integration.md: Findings (dangling_target, dangling_target_heading, only_candidates, missing_edges, no_causal_roles, one_sided_connectivity) in WorkbenchTodoUnion übersetzen. | `src/workbench/todoGenerator.ts` | – |
|
||||
| 4.2 | **buildWorkbenchModel: globalTodos** | Nach Verarbeitung der Template-Matches: `report.findings` an generateFindingsTodos übergeben; Ergebnis in `model.globalTodos` speichern. | `src/workbench/workbenchBuilder.ts` | 4.1 |
|
||||
| 4.3 | **WorkbenchModel.globalTodos** | Interface `WorkbenchModel` um `globalTodos?: WorkbenchTodoUnion[]` erweitern (falls noch nicht vorhanden). | `src/workbench/types.ts` | 4.2 |
|
||||
| 4.4 | **Deduplizierung** | Wenn ein Finding und ein Template-Todo dasselbe Problem beschreiben (z. B. dangling_target vs. missing_link für dieselbe Datei/Section): nur eines anzeigen (z. B. Findings-Todo priorisieren oder Template-Todo). | `src/workbench/workbenchBuilder.ts` | 4.2 |
|
||||
| 4.5 | **ChainWorkbenchModal: Section-Level Issues** | UI: Abschnitt „Section-Level Issues“ (oder „Findings“) mit globalTodos oberhalb der Template-Matches rendern. | `src/workbench/ChainWorkbenchModal.ts` (bzw. wo das Modal lebt) | 4.3 |
|
||||
| 4.6 | **Findings mit effective_type** | Optional: In generateFindingsTodos Kontext (file, heading) nutzen und effective_type in Todo-Beschreibung oder -Metadaten aufnehmen. | `src/workbench/todoGenerator.ts` | Phase 2 |
|
||||
|
||||
**Ergebnis Phase 4:** Workbench zeigt Section-Level Issues aus Findings; Konsistenz mit Chain Inspector; klare Trennung Findings vs. Template-Matches.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phase 5: UX, Commands, Optionale Erweiterungen
|
||||
|
||||
**Ziel:** Sichtbarkeit der Commands, optionale Filter/Actions, Dokumentation.
|
||||
|
||||
### 8.1 Tasks
|
||||
|
||||
| Nr | Task | Beschreibung | Dateien | Abhängigkeit |
|
||||
|----|------|--------------|---------|---------------|
|
||||
| 5.1 | **Commands ohne Editor (optional)** | Zusätzliche Commands „Mindnet: Inspect Chains“ / „Mindnet: Chain Workbench“ ohne editorCallback; bei Aufruf ohne geöffnete MD-Datei: Notice „Bitte zuerst eine Markdown-Datei öffnen.“ (FA-PL-12a). | `src/main.ts` | – |
|
||||
| 5.2 | **Notice bei fehlenden Dictionaries (Bestätigung)** | Sicherstellen, dass Inspect Chains und Chain Workbench bei fehlenden chain_roles/chain_templates mit Notice abbrechen (bereits in main.ts; nur prüfen). | `src/main.ts` | – |
|
||||
| 5.3 | **Benutzerhandbuch** | Kurzabschnitt: „Inspect Chains“ und „Chain Workbench“ sind nur bei geöffneter Markdown-Datei in der Befehlspalette verfügbar; bei fehlenden Dictionary-Dateien erscheint ein Hinweis. | docs (z. B. 01_Benutzerhandbuch.md) | – |
|
||||
| 5.4 | **Filter nach Severity (optional)** | Im Workbench Modal Filter „Nur Errors“ / „Errors und Warnings“ für globalTodos. | `src/workbench/ChainWorkbenchModal.ts` | 4.5 |
|
||||
| 5.5 | **Actions für no_causal_roles / missing_edges (optional)** | Fix-Actions „Edge-Type ändern“ für no_causal_roles, „Add edges to section“ für missing_edges (vgl. 10_Workbench_Findings_Integration.md Phase 5). | `src/commands/fixFindingsCommand.ts`, Workbench UI | 4.1 |
|
||||
| 5.6 | **slot_type_mismatch Finding (optional)** | Falls gewünscht: Finding ausgeben, wenn zugewiesener Node effective_type hat, der nicht in allowed_node_types liegt (aktuell verhindert Matching das). | `src/analysis/chainInspector.ts` oder templateMatching | Phase 2 |
|
||||
| 5.7 | **Intra-Note-Edges in Chain-Analyse (optional)** | detectIntraNoteEdges nutzen, um Chains innerhalb einer Note zu erkennen (WP26-Spec). | `src/analysis/chainInspector.ts`, graphIndex | FA-PL-03 |
|
||||
|
||||
**Ergebnis Phase 5:** Bessere Auffindbarkeit der Commands, dokumentiertes Verhalten, optionale Erweiterungen umsetzbar.
|
||||
|
||||
---
|
||||
|
||||
## 9. Abhängigkeitsgraph (Kurz)
|
||||
|
||||
```
|
||||
Phase 1 (Section-Parser, effective_type-Grundlage)
|
||||
→ Phase 2 (Chain Inspector effective_type)
|
||||
→ Phase 3 (Workbench Model/Todos Section-Type)
|
||||
→ Phase 4 (Findings → globalTodos) [4.1–4.3 unabhängig von Phase 2/3; 4.4–4.6 nutzen Kontext]
|
||||
→ Phase 5 (UX, Commands, Optionen)
|
||||
```
|
||||
|
||||
- **Phase 4.1–4.3** können parallel zu Phase 1–2 begonnen werden (Findings → Todos ohne Section-Type).
|
||||
- **Phase 4.4–4.6** (Deduplizierung, UI, effective_type in Findings) profitieren von Phase 2/3.
|
||||
|
||||
---
|
||||
|
||||
## 10. Test- und Abnahmepunkte
|
||||
|
||||
- **Phase 1:** Unit-Tests sectionParser (sectionType, blockId); Integration: buildNoteIndex liefert sectionType-Info.
|
||||
- **Phase 2:** Unit-Tests templateMatching (nodeMatchesSlot mit effectiveType); Integration: Inspect Chains Report mit effective_type in SlotAssignments.
|
||||
- **Phase 3:** Workbench zeigt fromSectionType/toSectionType in Todos; Edge-Vorschläge aus Schema.
|
||||
- **Phase 4:** Workbench zeigt globalTodos aus Findings; keine Doppelung mit Template-Todos; UI „Section-Level Issues“.
|
||||
- **Phase 5:** Commands ohne Editor zeigen Notice; Doku aktualisiert.
|
||||
|
||||
---
|
||||
|
||||
## 11. Referenzen
|
||||
|
||||
- **09_Chain_Inspector_Workbench_Lastenheft.md** – Alle Anforderungen (LH-CI-xx, LH-CW-xx).
|
||||
- **06_LH_WP26_Plugin_Integration.md** – FA-PL-01, FA-PL-08, FA-PL-12a.
|
||||
- **10_Workbench_Findings_Integration.md** – generateFindingsTodos, Todo-Typen, Phasen.
|
||||
- **CHAIN_INSPECTOR_V04_REPORT.md**, **CHAIN_INSPECTOR_V042_REPORT.md** – Template Matching, Profiles.
|
||||
- **WP26_Plugin_Interface_Specification.md** – Section-Type in Chain Workbench.
|
||||
|
||||
---
|
||||
|
||||
**Ende des Umsetzungsplans**
|
||||
214
docs/11_Manual_Test_Section_Type_Chain.md
Normal file
214
docs/11_Manual_Test_Section_Type_Chain.md
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
# Manuelle Testanleitung: Section-Type & Chain Inspector / Chain Workbench
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** Januar 2026
|
||||
**Ziel:** Die implementierten Funktionen (Section-Parser mit `sectionType`/`blockId`, effective_type im Chain Inspector und Chain Workbench) manuell in Obsidian prüfen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Voraussetzungen
|
||||
|
||||
- **Obsidian** mit aktiviertem Plugin **Mindnet Causal Assistant**
|
||||
- **Dictionary-Dateien** im Vault (z. B. unter `Dictionary/`):
|
||||
- `chain_templates.yaml`
|
||||
- `chain_roles.yaml`
|
||||
- `edge_vocabulary.md`
|
||||
- **Wichtig:** Die Commands **„Mindnet: Inspect Chains (Current Section)“** und **„Mindnet: Chain Workbench (Current Section)“** sind nur sichtbar, wenn eine **Markdown-Datei im Editor geöffnet** ist (editorCallback).
|
||||
|
||||
---
|
||||
|
||||
## 2. Automatische Tests ausführen (Empfehlung vor manuellen Tests)
|
||||
|
||||
```bash
|
||||
cd c:\Dev\cursor\mindnet_obsidian
|
||||
npm run build
|
||||
npx vitest run src/tests/mapping/sectionParser.test.ts src/tests/analysis/templateMatching.effectiveType.test.ts src/tests/analysis/templateMatching.test.ts src/tests/analysis/chainInspector.test.ts src/tests/workbench/todoGenerator.test.ts --reporter=verbose
|
||||
```
|
||||
|
||||
Erwartung: Alle genannten Test-Suites bestehen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Manuelle Tests
|
||||
|
||||
### 3.1 Section-Parser: `[!section]` und Block-ID
|
||||
|
||||
**Ziel:** Prüfen, dass `> [!section] type` und `^block-id` in Überschriften vom Parser erkannt werden (Grundlage für effective_type; direkte Anzeige im UI nur indirekt über Chain Inspector/Workbench).
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. **Neue Testnote anlegen** (z. B. `Test Section Type.md`):
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: concept
|
||||
---
|
||||
|
||||
## Kontext
|
||||
|
||||
> [!section] experience
|
||||
|
||||
Inhalt dieser Section. Der Section-Type ist "experience".
|
||||
|
||||
## Reflexion ^ref
|
||||
|
||||
> [!section] insight
|
||||
|
||||
Inhalt. Block-ID dieser Section ist "ref".
|
||||
```
|
||||
|
||||
2. **Speichern** und Note schließen/neu öffnen (optional, um Caching auszuschließen).
|
||||
|
||||
3. **Verifizierung:**
|
||||
- Chain Inspector/Workbench nutzen diese Section-Types (siehe 3.3 und 3.4).
|
||||
- Optional: In der **Developer Console** (Strg+Shift+I / Cmd+Option+I) nach Fehlern beim Parsing suchen; es sollten keine Fehler durch ungültige Callouts auftreten.
|
||||
|
||||
4. **Randfälle prüfen (optional):**
|
||||
- **Block-ID mit Bindestrichen/Unterstrichen:** z. B. `## Titel ^my-block_01` → sollte fehlerfrei bleiben.
|
||||
- **Zwei `[!section]` in einer Section:** Der **letzte** gesetzte Typ gilt („last wins“).
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Section-Type überschreibt Note-Type (effective_type)
|
||||
|
||||
**Ziel:** Sicherstellen, dass für eine Section mit `[!section] experience` der Typ **experience** für Chain-Matching verwendet wird, auch wenn das Frontmatter `type: concept` hat.
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. **Note anlegen** `EffectiveType Test.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: concept
|
||||
---
|
||||
|
||||
## Erlebnis
|
||||
|
||||
> [!section] experience
|
||||
|
||||
Dieser Abschnitt soll für Trigger-Slots als "experience" zählen.
|
||||
|
||||
> [!edge] wirkt_auf
|
||||
> [[Eine andere Note#Abschnitt]]
|
||||
```
|
||||
|
||||
2. **Zielnote** (z. B. `Eine andere Note.md`) mit Section „Abschnitt“ und ggf. weiterem Inhalt/Edges anlegen, sodass eine Kette entstehen kann.
|
||||
|
||||
3. **Cursor** in die Section **„Erlebnis“** von `EffectiveType Test.md` setzen.
|
||||
|
||||
4. **Command ausführen:** **Mindnet: Inspect Chains (Current Section)** (Strg+P / Cmd+P → „Inspect Chains“ eingeben).
|
||||
|
||||
5. **Developer Console öffnen** (Strg+Shift+I / Cmd+Option+I) und den **Chain Inspector Report** ansehen.
|
||||
|
||||
6. **Erwartung:**
|
||||
- In `templateMatches` erscheint für die aktuelle Section (z. B. als „trigger“) der Typ **experience** (nicht „concept“) in den Slot-Zuordnungen (`noteType`/effective_type).
|
||||
- Wenn kein Template-Match gefunden wird (z. B. fehlende Kette), reicht die Prüfung: Beim nächsten Test mit vollständiger Kette (3.3) muss der Trigger-Slot mit „experience“ gefüllt sein.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Chain Inspector: Inspect Chains (Current Section)
|
||||
|
||||
**Ziel:** Vollständigen Ablauf von „Inspect Chains“ prüfen und Template-Matches inkl. effective_type prüfen.
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. **Test-Vault mit kausaler Kette vorbereiten:**
|
||||
- **Note A** (z. B. Trigger): eine Section mit `> [!section] experience` (oder Frontmatter `type: experience`) und Edge zu Note B.
|
||||
- **Note B** (Transformation): Section mit `type: insight` bzw. `[!section] insight`, Edge zu Note C.
|
||||
- **Note C** (Outcome): Section mit `type: decision` bzw. `[!section] decision`.
|
||||
|
||||
2. **Note A** im Editor öffnen, Cursor in die Section mit den ausgehenden Edges setzen.
|
||||
|
||||
3. **Command:** **Mindnet: Inspect Chains (Current Section)** ausführen.
|
||||
|
||||
4. **In der Developer Console prüfen:**
|
||||
- Ob ein **Report** mit `templateMatches` erscheint.
|
||||
- Ob ein Template (z. B. `trigger_transformation_outcome`) gematcht wird.
|
||||
- Ob in `slotAssignments` die Slots (z. B. trigger, transformation, outcome) mit den erwarteten Dateien/Sections und **Typen** (experience, insight, decision) gefüllt sind.
|
||||
- Ob **Findings** (z. B. `missing_slot_*`, `weak_chain_roles`) plausibel sind.
|
||||
|
||||
5. **Erwartung:**
|
||||
- Keine Fehlermeldung; Report enthält `context`, `neighbors`, `templateMatches`, ggf. `findings`.
|
||||
- Wenn Section-Type gesetzt ist, erscheint dieser Typ in der Slot-Zuordnung (nicht nur der Note-Type).
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Chain Workbench: Chain Workbench (Current Section)
|
||||
|
||||
**Ziel:** Chain Workbench öffnen und Anzeige von Matches/TODOs prüfen.
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. **Gleiche Test-Situation** wie in 3.3 (Note A mit Section geöffnet, Cursor in der Section).
|
||||
|
||||
2. **Command:** **Mindnet: Chain Workbench (Current Section)** ausführen.
|
||||
|
||||
3. **Modal prüfen:**
|
||||
- **Template Matches** werden angezeigt (z. B. „trigger_transformation_outcome“).
|
||||
- **Status** der Matches (complete / near_complete / partial / weak) ist lesbar.
|
||||
- **TODOs** (z. B. missing_slot, missing_link) erscheinen, falls die Kette unvollständig ist.
|
||||
- Kein Absturz, keine leere Anzeige ohne Hinweis.
|
||||
|
||||
4. **Erwartung:**
|
||||
- Workbench öffnet sich; Matches und TODOs sind konsistent mit dem Chain Inspector Report.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Fehlende Dictionary-Dateien (Notice)
|
||||
|
||||
**Ziel:** Prüfen, dass bei fehlenden Chain-Dictionaries ein **Notice** erscheint und der Command abbricht.
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. In den **Plugin-Einstellungen** einen **ungültigen Pfad** für **Chain Roles** oder **Chain Templates** eintragen (z. B. `Dictionary/chain_roles_nicht_vorhanden.yaml`).
|
||||
|
||||
2. Eine Markdown-Note öffnen und **Mindnet: Inspect Chains (Current Section)** ausführen.
|
||||
|
||||
3. **Erwartung:**
|
||||
- Ein **Notice** erscheint (z. B. „Chain Roles nicht geladen“ oder „Chain Templates nicht geladen“).
|
||||
- Kein stilles Weiterlaufen mit leerem oder irreführendem Report.
|
||||
|
||||
4. **Pfad wieder auf eine gültige Datei setzen** und erneut testen; Report soll wieder normal erscheinen.
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Sichtbarkeit der Commands (editorCallback)
|
||||
|
||||
**Ziel:** Bestätigen, dass Inspect Chains und Chain Workbench nur bei geöffneter MD-Datei verfügbar sind.
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. **Alle Markdown-Editor-Tabs schließen** (oder in einen nicht-Markdown-Tab wechseln, z. B. Einstellungen).
|
||||
|
||||
2. **Befehlspalette** öffnen (Strg+P / Cmd+P) und nach „Inspect“ oder „Chain“ suchen.
|
||||
|
||||
3. **Erwartung:**
|
||||
- „Mindnet: Inspect Chains (Current Section)“ und „Mindnet: Chain Workbench (Current Section)“ sind **ausgegraut** oder nicht auswählbar.
|
||||
|
||||
4. **Eine Markdown-Note öffnen.**
|
||||
|
||||
5. **Befehlspalette** erneut öffnen und dieselben Befehle suchen.
|
||||
|
||||
6. **Erwartung:**
|
||||
- Die Befehle sind **aktiv** und auswählbar.
|
||||
|
||||
---
|
||||
|
||||
## 4. Kurz-Checkliste (Manuell)
|
||||
|
||||
| Nr | Test | Erwartung |
|
||||
|----|------|-----------|
|
||||
| 1 | Section-Parser: Note mit `[!section]` und `^block-id` | Keine Fehler; Chain-Features nutzen effective_type. |
|
||||
| 2 | effective_type: Section-Type überschreibt Note-Type | Im Report erscheint Section-Type (z. B. experience) in Slot-Zuordnung. |
|
||||
| 3 | Inspect Chains ausführen | Report in Console mit templateMatches und ggf. findings. |
|
||||
| 4 | Chain Workbench öffnen | Modal mit Matches und TODOs. |
|
||||
| 5 | Ungültiger Dictionary-Pfad | Notice, kein stiller Abbruch. |
|
||||
| 6 | Commands ohne geöffnete MD-Datei | Befehle ausgegraut. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Referenzen
|
||||
|
||||
- **Automatische Tests:** `src/tests/mapping/sectionParser.test.ts`, `src/tests/analysis/templateMatching.effectiveType.test.ts`
|
||||
- **Chain Workbench Testing:** `docs/08_Testing_Chain_Workbench.md`
|
||||
- **Lastenheft Chain Inspector & Workbench:** `docs/09_Chain_Inspector_Workbench_Lastenheft.md`
|
||||
113
docs/12_Heading_Block_Link_Recommendation.md
Normal file
113
docs/12_Heading_Block_Link_Recommendation.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Bewertung & Empfehlung: Überschrift vs. Block-Link (Heading-Match)
|
||||
|
||||
**Datum:** Januar 2026
|
||||
**Kontext:** Exaktes Heading-Matching in Inspect Chains / Chain Workbench vs. Obsidian-Standard und Interview-Assistent.
|
||||
|
||||
---
|
||||
|
||||
## 1. Obsidian-Standard (Dokumentation & beobachtetes Verhalten)
|
||||
|
||||
### 1.1 Drei relevante Link-Formen (bei Überschrift mit Block-ID)
|
||||
|
||||
Bei einer Zeile `## Überschrift ^Block` in der Zieldatei:
|
||||
|
||||
| Link | Verhalten beim Klick |
|
||||
|------|----------------------|
|
||||
| **`[[Titel#Überschrift Block]]`** (ohne `^`) | Obsidian **erzeugt diese Form**, wenn man manuell per UI auf die Überschrift verlinkt – das Zeichen `^` wird **immer entfernt**. Springt auf die Überschrift, **highlighted den kompletten Text unter der Überschrift**. |
|
||||
| **`[[Titel#Überschrift]]`** (nur Text, ohne „ Block“) | Springt **in die Note, aber nicht auf die Überschrift**. Entspricht dem aktuellen Interview-Assistenten. |
|
||||
| **`[[Titel#^Block]]`** (Block-Link) | Springt auf die Überschrift, **highlighted nur die Überschrift selbst** (nicht den Abschnitt darunter). Funktioniert auch, wenn man `^` manuell einträgt (Obsidian entfernt `^` nur bei UI-erzeugten Links). |
|
||||
|
||||
### 1.2 Beobachtetes Verhalten
|
||||
|
||||
- **Obsidian entfernt bei UI-Links das `^`:** Ein manuell gesetzter Wikilink auf eine Überschrift mit Block-ID wird zu `[[Titel#Überschrift Block]]` (mit Leerzeichen, ohne Caret).
|
||||
- **Drei Schreibweisen bezeichnen dieselbe Sektion:** `Überschrift`, `Überschrift ^Block` (im Quelldokument), `Überschrift Block` (in Obsidian-Links). Für Matching müssen alle drei als dieselbe Sektion gelten.
|
||||
|
||||
---
|
||||
|
||||
## 2. Aktuelles Verhalten im Plugin
|
||||
|
||||
### 2.1 Section-Parser & Graph-Index
|
||||
|
||||
- **sectionParser:** Speichert `heading` als **vollständige Zeile** nach den `#`, also z. B. `"Überschrift ^block"` (inkl. Block-ID).
|
||||
- **graphIndex `parseTarget`:**
|
||||
- Bei `[[Datei#X]]`: `target.heading = X` (exakt der Teil nach `#`).
|
||||
- Bei `[[#^block-id]]`: Auflösung über Sektionen → `heading = section.heading` (ebenfalls voller Text, inkl. ` ^block`).
|
||||
- **Section-Nodes:** Werden aus `section.heading` gebaut → ebenfalls voller Text (z. B. `"Überschrift ^block"`).
|
||||
|
||||
### 2.2 Interview-Assistent (LinkTargetPicker + renderNoteEdges)
|
||||
|
||||
- **getHeadingsWithSectionTypes** (targetTypeResolver): Liest Überschriften per **eigenem Regex** und **entfernt** den Block-Teil: `(.+?)(?:\s+\^[\w-]+)?\s*$` → `match[2]` ist nur der Text (z. B. `"Überschrift"`).
|
||||
- **LinkTargetPicker:** Setzt `linkTarget = basename + "#" + h.heading` → es wird **nur** `[[Note#Überschrift]]` erzeugt (ohne „ Block“ / ` ^block`).
|
||||
- **renderNoteEdges:** Schreibt `[[${toNote}]]`; `toNote` ist bereits `"Note"` oder `"Note#Überschrift"`.
|
||||
|
||||
**Fazit:** Der Interview-Assistent erzeugt `[[Note#Überschrift]]`. In Obsidian springt dieser Link **in die Note, aber nicht auf die Überschrift** (siehe Abschnitt 1). Obsidian-typisches Springen auf die Überschrift (inkl. Highlight des Abschnitts) liefert nur `[[Titel#Überschrift Block]]` (UI-erzeugt, ohne `^`).
|
||||
|
||||
### 2.3 Inspect Chains & Chain Workbench
|
||||
|
||||
- **chainInspector** vergleicht überall **strikt:**
|
||||
`edge.target.heading === context.heading` und
|
||||
`edge.source.sectionHeading === context.heading`.
|
||||
- **dangling_target_heading:** Prüfung gegen `metadataCache.getFileCache(...).headings` mit
|
||||
`h.heading === targetHeading` (ebenfalls exakt).
|
||||
- **Chain Workbench** nutzt dieselben Konzepte (context/assignment mit `heading`); keine Normalisierung.
|
||||
|
||||
**Folge:** Sobald eine Seite `"Überschrift ^block"` und ein Link `[[Titel#Überschrift]]` vorkommt (oder umgekehrt), stimmen die Strings nicht überein → kein Match, ggf. falscher `dangling_target_heading`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Bewertung
|
||||
|
||||
| Aspekt | Bewertung |
|
||||
|--------|-----------|
|
||||
| **Obsidian (UI)** | Beim manuellen Verlinken auf eine Überschrift mit Block-ID: Link wird zu `[[Titel#Überschrift Block]]` (ohne `^`). Klick springt auf Überschrift und highlighted den Abschnitt. |
|
||||
| **Interview-Assistent** | Erzeugt `[[Note#Überschrift]]`. In Obsidian springt der Link **nicht** auf die Überschrift (nur in die Note). Besseres Spring-Verhalten hätte `[[Note#Überschrift Block]]` (Obsidian-Stil) oder `[[Note#^Block]]`. |
|
||||
| **Inspect Chains / Workbench** | Exaktes Match ist zu streng: „Überschrift“, „Überschrift ^Block“, „Überschrift Block“ bezeichnen dieselbe Sektion. |
|
||||
| **Risiko** | Ohne Normalisierung matchen diese drei Schreibweisen nicht; falsche Findings / fehlende Zuordnung. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Empfehlung
|
||||
|
||||
### 4.1 Eine Baustelle zuerst (keine Überladung)
|
||||
|
||||
Um nicht zu viele Änderungen gleichzeitig zu öffnen:
|
||||
|
||||
- **Nur eine Baustelle:** **Normalisierung in Inspect Chains & Chain Workbench** (zentrale Normalisierungsfunktion + alle Heading-Vergleiche und `dangling_target_heading` auf normalisierte Form umstellen).
|
||||
- **Interview-Assistent vorerst unverändert:** Weiter `[[Note#Überschrift]]` ausgeben. Eine spätere, **optionale** Anpassung (z. B. Ausgabe `[[Note#Überschrift Block]]` im Obsidian-Stil, damit der Klick auf die Überschrift springt) kann separat erfolgen.
|
||||
|
||||
### 4.2 Normalisierungsregel (für Vergleiche)
|
||||
|
||||
Drei Schreibweisen sollen als **dieselbe Sektion** gelten:
|
||||
|
||||
- `Überschrift` (Interview, reiner Text)
|
||||
- `Überschrift ^Block` (Quelldokument / sectionParser)
|
||||
- `Überschrift Block` (Obsidian-UI-Link, ohne `^`)
|
||||
|
||||
**Kanonische Form für Vergleiche:**
|
||||
|
||||
1. Am Ende **Block-Suffix mit Caret** entfernen: `\s+\^[a-zA-Z0-9_-]+$` → z. B. `"Überschrift ^Block"` → `"Überschrift"`.
|
||||
2. Danach **ein einzelnes Wort am Ende** (nur Buchstaben, Zahlen, Bindestrich, Unterstrich) optional entfernen: `\s+[a-zA-Z0-9_-]+$` → z. B. `"Überschrift Block"` → `"Überschrift"`.
|
||||
|
||||
So werden alle drei Varianten auf `"Überschrift"` abgebildet. **Randfall:** Eine Überschrift, die wirklich „X Y“ heißt (ohne Block-ID), würde ebenfalls zu „X“ normalisiert und könnte mit „X“ matchen; in der Praxis selten. Die Funktion **nur** für Vergleiche/Prüfungen nutzen, nicht für Anzeige oder gespeicherte Links.
|
||||
|
||||
### 4.3 Konkrete Maßnahmen (nur diese eine Baustelle)
|
||||
|
||||
1. **Eine zentrale Normalisierungsfunktion** (z. B. in `linkHelpers` oder eigenem Modul): Eingabe = Überschrift-String, Ausgabe = kanonische Form wie oben.
|
||||
|
||||
2. **Inspect Chains (chainInspector):** Alle Vergleiche mit `heading` / `sectionHeading` auf die **normalisierte** Form umstellen; bei **dangling_target_heading** `targetHeading` und Cache-Headings vor dem Vergleich normalisieren.
|
||||
|
||||
3. **Chain Workbench:** Dieselbe Normalisierung überall, wo Headings verglichen werden.
|
||||
|
||||
4. **graphIndex / Interview:** Keine Änderung in dieser Baustelle.
|
||||
|
||||
### 4.4 Optional später (separate Baustelle)
|
||||
|
||||
- **Interview-Assistent:** Wenn gewünscht, Links so erzeugen, dass Obsidian auf die Überschrift springt: z. B. `[[Note#Überschrift Block]]` (Block-Teil ohne `^`, wie Obsidian-UI) statt `[[Note#Überschrift]]`. Dann würde der Klick auf den vom Assistenten gesetzten Link dasselbe tun wie bei manuell gesetzten Links.
|
||||
|
||||
---
|
||||
|
||||
## 5. Kurzfassung
|
||||
|
||||
- **Obsidian (beobachtet):** Manueller Link auf Überschrift mit Block-ID → `[[Titel#Überschrift Block]]` (ohne `^`). Klick springt auf Überschrift und highlighted den Abschnitt. `[[Titel#Überschrift]]` springt nur in die Note, nicht auf die Überschrift. `[[Titel#^Block]]` highlighted nur die Überschrift.
|
||||
- **Eine Baustelle:** Nur **Normalisierung in Inspect Chains & Chain Workbench** umsetzen (zentrale Funktion: Block-Suffix mit `^` und ggf. ein trailinges Wort entfernen; alle Heading-Vergleiche und `dangling_target_heading` auf diese Form umstellen). Interview-Assistent und graphIndex in dieser Runde **nicht** anfassen.
|
||||
- **Optional später:** Interview-Assistent so anpassen, dass er z. B. `[[Note#Überschrift Block]]` erzeugt (Obsidian-Stil), damit der Klick auf den Link auf die Überschrift springt.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# WP-26 Plugin-Schnittstellenspezifikation
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 25. Januar 2026
|
||||
**Status:** Implementierungsleitfaden für mindnet_obsidian Plugin
|
||||
**Version:** 1.1
|
||||
**Datum:** 28. Januar 2026
|
||||
**Status:** Implementierungsleitfaden für mindnet_obsidian Plugin (aktualisiert: Interview Edge-per-Section, Note#Abschnitt, Commands)
|
||||
**Basis:** Lastenheft WP-26 v1.3 (Section Types & Intra-Note-Edges)
|
||||
|
||||
---
|
||||
|
|
@ -239,10 +239,34 @@ Text der Reflexion...
|
|||
}
|
||||
```
|
||||
|
||||
**Implementierung:**
|
||||
- Erweitere Interview-Steps um Section-Type-Optionen
|
||||
- Generiere automatisch Block-IDs basierend auf Step-Keys
|
||||
- Füge `[!section]` Callouts automatisch ein, wenn `sectionType` definiert ist
|
||||
**Implementierung:** (umgesetzt)
|
||||
- Interview-Steps um Section-Type-Optionen erweitert
|
||||
- Block-IDs automatisch aus Step-Keys generiert
|
||||
- `[!section]` Callouts werden eingefügt, wenn `sectionType` definiert ist
|
||||
|
||||
#### 3.3.2 Edge-Typen pro Sektion und Note-Edges (implementiert, Jan 2026)
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `src/ui/SectionEdgesOverviewModal.ts`
|
||||
- `src/ui/LinkTargetPickerModal.ts`
|
||||
- `src/ui/InterviewWizardModal.ts`
|
||||
- `src/interview/renderer.ts`
|
||||
- `src/parser/parseRelLinks.ts`
|
||||
- `src/mapping/sectionParser.ts`
|
||||
- `src/mapping/semanticMappingBuilder.ts`
|
||||
- `src/mapping/updateMappingBlocks.ts`
|
||||
|
||||
**Umsetzung:**
|
||||
|
||||
1. **SectionEdgesOverviewModal:** Am Ende des Interviews werden alle Section-Edges und Note-Edges angezeigt; Edge-Typen können angepasst werden. Note-Ziele werden als vollständiger Link (z. B. `Note#Abschnitt`) gespeichert.
|
||||
|
||||
2. **LinkTargetPickerModal:** Nach Auswahl einer Note im Entity Picker kann gewählt werden: ganze Note oder konkreter Abschnitt (`Note#Abschnitt`). Der gewählte Link wird in Inline-Micro-Edging und in den Übersichts-Dialog übernommen.
|
||||
|
||||
3. **Renderer:** `RenderOptions.noteEdges` (Map von Block-ID → toNote/`Note#Abschnitt` → edgeType). Note-Edges werden pro Sektion als `>> [!edge] type` und `>> [[Note#Abschnitt]]` im Semantic-Mapping-Block ausgegeben.
|
||||
|
||||
4. **Links mit Abschnitt:** `parseRelLinks` und `sectionParser.extractWikilinks` behalten `#Abschnitt`; Mapping-Blöcke und Text-Links schreiben `[[Note#Abschnitt]]` bzw. `[[rel:type|Note#Abschnitt]]`.
|
||||
|
||||
**Backend-Konformität:** Edge-Callouts unterstützen `[[Target#Section]]` (vgl. Abschnitt 2.2). Das Plugin erzeugt und erhält diese Form konsistent.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -499,11 +523,23 @@ interface SectionTypeModalProps {
|
|||
|
||||
## 6. Commands
|
||||
|
||||
### 6.1 Neue Commands
|
||||
### 6.1 Bereits implementierte Commands (Stand: Jan 2026)
|
||||
|
||||
#### 6.1.1 "Add Section Type"
|
||||
| Command-ID | Name | Kontext | Beschreibung |
|
||||
|------------|------|---------|--------------|
|
||||
| `mindnet-change-edge-type` | Mindnet: Edge-Type ändern | **editorCallback** (nur bei geöffneter MD-Datei) | Kantentyp für Einzellink, Auswahl oder ganze Note setzen; neuer Link mit Kantentyp. Modul: `edgeTypeSelector.ts`. |
|
||||
| `mindnet-build-semantic-mappings` | Mindnet: Build semantic mapping blocks (by section) | **editorCallback** | Pro Sektion Semantic-Mapping-Blöcke bauen/überarbeiten. Modul: `semanticMappingBuilder.ts`. Bei Cursor in Link / Auswahl wird stattdessen Edge-Type-Selector genutzt. |
|
||||
| `mindnet-fix-findings` | Mindnet: Fix Findings (Current Section) | **editorCallback** | Fehlende Links einfügen, Kandidaten-Edges bestätigen. Modul: `fixFindingsCommand.ts`. |
|
||||
|
||||
**Command-ID:** `mindnet:add-section-type`
|
||||
**Hinweis:** `editorCallback`-Commands erscheinen in der Befehlspalette nur, wenn eine Markdown-Datei im Editor aktiv ist. Ohne geöffnete Note sind sie ausgegraut. Siehe Lastenheft FA-PL-12a (Sichtbarkeit).
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Geplante / noch nicht umgesetzte Commands
|
||||
|
||||
#### 6.2.1 "Add Section Type"
|
||||
|
||||
**Command-ID (geplant):** `mindnet:add-section-type`
|
||||
|
||||
**Funktionalität:**
|
||||
- Fügt `[!section]` Callout zur aktuellen Section hinzu
|
||||
|
|
@ -512,9 +548,9 @@ interface SectionTypeModalProps {
|
|||
|
||||
---
|
||||
|
||||
#### 6.1.2 "Add Block ID to Heading"
|
||||
#### 6.2.2 "Add Block ID to Heading"
|
||||
|
||||
**Command-ID:** `mindnet:add-block-id`
|
||||
**Command-ID (geplant):** `mindnet:add-block-id`
|
||||
|
||||
**Funktionalität:**
|
||||
- Fügt Block-ID zur aktuellen Überschrift hinzu
|
||||
|
|
@ -523,9 +559,9 @@ interface SectionTypeModalProps {
|
|||
|
||||
---
|
||||
|
||||
#### 6.1.3 "Insert Intra-Note Edge"
|
||||
#### 6.2.3 "Insert Intra-Note Edge"
|
||||
|
||||
**Command-ID:** `mindnet:insert-intra-note-edge`
|
||||
**Command-ID (geplant):** `mindnet:insert-intra-note-edge`
|
||||
|
||||
**Funktionalität:**
|
||||
- Fügt `[!edge]` Callout mit Block-Reference hinzu
|
||||
|
|
@ -534,9 +570,9 @@ interface SectionTypeModalProps {
|
|||
|
||||
---
|
||||
|
||||
### 6.2 Erweiterte Commands
|
||||
### 6.3 Erweiterte Commands (Interview, Workbench)
|
||||
|
||||
#### 6.2.1 Chain Workbench
|
||||
#### 6.3.1 Chain Workbench
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `src/commands/chainWorkbenchCommand.ts`
|
||||
|
|
@ -548,7 +584,7 @@ interface SectionTypeModalProps {
|
|||
|
||||
---
|
||||
|
||||
#### 6.2.2 Interview Wizard
|
||||
#### 6.3.2 Interview Wizard
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `src/interview/renderer.ts`
|
||||
|
|
@ -791,7 +827,12 @@ export interface GraphSchema {
|
|||
|
||||
**Ende der Spezifikation**
|
||||
|
||||
**Implementierungsstand (Kurz, Jan 2026):**
|
||||
- Phase 1–3: Kern-Parsing, UI, Interview (inkl. Edge-per-Section, Note#Abschnitt) weitgehend umgesetzt.
|
||||
- Commands: „Edge-Type ändern“, „Build semantic mapping blocks“, „Fix Findings“ implementiert (editorCallback); „Add Section Type“, „Add Block ID“, „Insert Intra-Note Edge“ noch offen.
|
||||
- Details: Lastenheft `06_LH_WP26_Plugin_Integration.md` (FA-PL-12, FA-PL-14, Abschnitt 8 Nächste Schritte).
|
||||
|
||||
**Nächste Schritte:**
|
||||
1. Review der Spezifikation
|
||||
2. Priorisierung der Implementierungsphasen
|
||||
3. Start mit Phase 1 (Grundlagen)
|
||||
1. FA-PL-12a: Sichtbarkeit der Edge-Commands (Dokumentation und/oder Command ohne editorCallback).
|
||||
2. Geplante Commands (Add Section Type, Add Block ID, Insert Intra-Note Edge) priorisieren.
|
||||
3. Abnahme Note#Abschnitt mit Backend; ggf. Lint für konsistente Abschnitts-Links.
|
||||
|
|
|
|||
36
docs/snippet_section_callout.css
Normal file
36
docs/snippet_section_callout.css
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Unauffällige Formatierung für > [!section] type (Mindnet)
|
||||
* Inhalt in .obsidian/snippets/mindnet.css einfügen und Snippet aktivieren.
|
||||
*/
|
||||
|
||||
/* [!section] – möglichst unauffällig: kein Kasten, kein Stift, wie ein kleines Label */
|
||||
.callout[data-callout="section"] {
|
||||
--callout-color: 128, 128, 128;
|
||||
--callout-icon: none;
|
||||
border: none;
|
||||
border-left: 1px solid rgba(var(--callout-color), 0.25);
|
||||
background-color: transparent;
|
||||
padding: 0.1em 0 0.1em 0.5em;
|
||||
margin: 0.25em 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.callout[data-callout="section"] .callout-title {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.callout[data-callout="section"] .callout-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.callout[data-callout="section"] .callout-title-inner {
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.callout[data-callout="section"] .callout-content {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -9,11 +9,18 @@ import type { IndexedEdge, SectionNode } from "./graphIndex";
|
|||
import { buildNoteIndex, loadNeighborNote } from "./graphIndex";
|
||||
import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types";
|
||||
import { splitIntoSections } from "../mapping/sectionParser";
|
||||
import { normalizeLinkTarget } from "../unresolvedLink/linkHelpers";
|
||||
import { normalizeLinkTarget, headingsMatch } from "../unresolvedLink/linkHelpers";
|
||||
import type { EdgeVocabulary } from "../vocab/types";
|
||||
import { parseEdgeVocabulary } from "../vocab/parseEdgeVocabulary";
|
||||
import { VocabularyLoader } from "../vocab/VocabularyLoader";
|
||||
import { applySeverityPolicy } from "./severityPolicy";
|
||||
import { getLogger, loggerRegistry } from "../utils/logger";
|
||||
|
||||
// Don't create logger instance at import time - create it when needed
|
||||
// This ensures it always reads current log levels from registry
|
||||
function getChainInspectorLogger() {
|
||||
return getLogger("chainInspector");
|
||||
}
|
||||
|
||||
export interface InspectorOptions {
|
||||
includeNoteLinks: boolean;
|
||||
|
|
@ -21,6 +28,10 @@ export interface InspectorOptions {
|
|||
maxDepth: number;
|
||||
direction: "forward" | "backward" | "both";
|
||||
maxTemplateMatches?: number; // Optional: limit number of template matches (default: 3)
|
||||
/** Default max distinct matches per template. Overridable by chain_templates.yaml defaults.matching.max_matches_per_template. */
|
||||
maxMatchesPerTemplateDefault?: number; // default: 2
|
||||
/** When true, log template-matching details (candidates per slot, collected/complete counts, returned matches) to console. */
|
||||
debugLogging?: boolean;
|
||||
}
|
||||
|
||||
export interface Finding {
|
||||
|
|
@ -196,7 +207,7 @@ function getNeighbors(
|
|||
// Match exact section (file + heading) OR note-level link (file only, heading null)
|
||||
const targetsCurrentSection =
|
||||
matchesCurrentFile(edge.target.file) &&
|
||||
(edge.target.heading === currentSection.heading ||
|
||||
(headingsMatch(edge.target.heading, currentSection.heading) ||
|
||||
(edge.target.heading === null && currentSection.heading !== null));
|
||||
|
||||
if (targetsCurrentSection) {
|
||||
|
|
@ -221,7 +232,7 @@ function getNeighbors(
|
|||
// Check if edge originates from current section
|
||||
const sourceMatches =
|
||||
("sectionHeading" in edge.source
|
||||
? edge.source.sectionHeading === currentSection.heading &&
|
||||
? headingsMatch(edge.source.sectionHeading, currentSection.heading) &&
|
||||
edge.source.file === currentSection.file
|
||||
: edge.scope === "note" && edge.source.file === currentSection.file) &&
|
||||
edge.source.file === currentSection.file;
|
||||
|
|
@ -307,7 +318,7 @@ function traverseForward(
|
|||
for (const edge of edges) {
|
||||
const sourceMatches =
|
||||
("sectionHeading" in edge.source
|
||||
? edge.source.sectionHeading === current.heading &&
|
||||
? headingsMatch(edge.source.sectionHeading, current.heading) &&
|
||||
edge.source.file === current.file
|
||||
: edge.scope === "note" && edge.source.file === current.file) &&
|
||||
edge.source.file === current.file;
|
||||
|
|
@ -376,7 +387,7 @@ function traverseBackward(
|
|||
for (const edge of edges) {
|
||||
const targetsCurrentNode =
|
||||
matchesCurrentFile(edge.target.file) &&
|
||||
(edge.target.heading === current.heading ||
|
||||
(headingsMatch(edge.target.heading, current.heading) ||
|
||||
(edge.target.heading === null && current.heading !== null));
|
||||
|
||||
if (targetsCurrentNode) {
|
||||
|
|
@ -434,9 +445,9 @@ function computeFindings(
|
|||
): Finding[] {
|
||||
const findings: Finding[] = [];
|
||||
|
||||
// Find current section content
|
||||
// Find current section content (normalized heading match for block-id variants)
|
||||
const currentSection = sections.find(
|
||||
(s) => s.file === context.file && s.heading === context.heading
|
||||
(s) => s.file === context.file && headingsMatch(s.heading, context.heading)
|
||||
);
|
||||
|
||||
if (!currentSection) {
|
||||
|
|
@ -450,7 +461,7 @@ function computeFindings(
|
|||
|
||||
const sourceMatches =
|
||||
"sectionHeading" in edge.source
|
||||
? edge.source.sectionHeading === context.heading &&
|
||||
? headingsMatch(edge.source.sectionHeading, context.heading) &&
|
||||
edge.source.file === context.file
|
||||
: false;
|
||||
|
||||
|
|
@ -498,7 +509,7 @@ function computeFindings(
|
|||
const incoming = filteredAllEdges.filter((edge) => {
|
||||
// Don't manually filter candidates here - filterEdges already did that based on options.includeCandidates
|
||||
const fileMatches = matchesCurrentFile(edge.target.file);
|
||||
const headingMatches = edge.target.heading === context.heading ||
|
||||
const headingMatches = headingsMatch(edge.target.heading, context.heading) ||
|
||||
(edge.target.heading === null && context.heading !== null);
|
||||
return fileMatches && headingMatches;
|
||||
});
|
||||
|
|
@ -511,14 +522,14 @@ function computeFindings(
|
|||
const outgoing = filteredCurrentEdges.filter((edge) => {
|
||||
const sourceMatches =
|
||||
"sectionHeading" in edge.source
|
||||
? edge.source.sectionHeading === context.heading &&
|
||||
? headingsMatch(edge.source.sectionHeading, context.heading) &&
|
||||
edge.source.file === context.file
|
||||
: edge.scope === "note" && edge.source.file === context.file;
|
||||
return sourceMatches;
|
||||
});
|
||||
|
||||
// Debug logging for findings (use effective counts that match report.neighbors)
|
||||
console.log(`[Chain Inspector] computeFindings: incomingEffective=${incoming.length}, outgoingEffective=${outgoing.length}, allEdges=${allEdges.length}, filteredAllEdges=${filteredAllEdges.length}`);
|
||||
getChainInspectorLogger().debug(`computeFindings: incomingEffective=${incoming.length}, outgoingEffective=${outgoing.length}, allEdges=${allEdges.length}, filteredAllEdges=${filteredAllEdges.length}`);
|
||||
|
||||
if (incoming.length > 0 && outgoing.length === 0) {
|
||||
findings.push({
|
||||
|
|
@ -547,7 +558,7 @@ function computeFindings(
|
|||
(edge) =>
|
||||
edge.scope === "candidate" &&
|
||||
("sectionHeading" in edge.source
|
||||
? edge.source.sectionHeading === context.heading &&
|
||||
? headingsMatch(edge.source.sectionHeading, context.heading) &&
|
||||
edge.source.file === context.file
|
||||
: false)
|
||||
);
|
||||
|
|
@ -597,7 +608,7 @@ function computeFindings(
|
|||
// Use file cache to check headings
|
||||
const headings = targetContent.headings || [];
|
||||
const headingExists = headings.some(
|
||||
(h) => h.heading === targetHeading
|
||||
(h) => headingsMatch(h.heading, targetHeading)
|
||||
);
|
||||
|
||||
if (!headingExists) {
|
||||
|
|
@ -704,7 +715,7 @@ export async function inspectChains(
|
|||
// Only consider edges from current context
|
||||
if (
|
||||
("sectionHeading" in edge.source
|
||||
? edge.source.sectionHeading === context.heading &&
|
||||
? headingsMatch(edge.source.sectionHeading, context.heading) &&
|
||||
edge.source.file === context.file
|
||||
: edge.scope === "note" && edge.source.file === context.file) &&
|
||||
edge.source.file === context.file
|
||||
|
|
@ -727,14 +738,14 @@ export async function inspectChains(
|
|||
if (sourcePath === context.file) continue; // Skip self
|
||||
notesLinkingToCurrent.add(sourcePath);
|
||||
}
|
||||
console.log(`[Chain Inspector] Found ${notesLinkingToCurrent.size} notes linking to current note via getBacklinksForFile`);
|
||||
getChainInspectorLogger().debug(`Found ${notesLinkingToCurrent.size} notes linking to current note via getBacklinksForFile`);
|
||||
} else {
|
||||
console.log("[Chain Inspector] getBacklinksForFile returned null/undefined");
|
||||
getChainInspectorLogger().debug("getBacklinksForFile returned null/undefined");
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: if getBacklinksForFile is not available, use manual scan
|
||||
// This should rarely happen, but provides compatibility
|
||||
console.warn("getBacklinksForFile not available, falling back to manual scan", e);
|
||||
getChainInspectorLogger().warn("getBacklinksForFile not available, falling back to manual scan", e);
|
||||
|
||||
const currentNoteBasename = (currentFile as TFile).basename;
|
||||
const currentNotePath = context.file;
|
||||
|
|
@ -789,7 +800,7 @@ export async function inspectChains(
|
|||
const outgoingNeighborFileMap = new Map<string, TFile>(); // original target -> resolved TFile
|
||||
|
||||
// Resolve outgoing targets (may be basenames without folder)
|
||||
console.log(`[Chain Inspector] Loading outgoing neighbor notes: ${outgoingTargets.size}`);
|
||||
getChainInspectorLogger().debug(`Loading outgoing neighbor notes: ${outgoingTargets.size}`);
|
||||
for (const targetFile of outgoingTargets) {
|
||||
if (targetFile === context.file) continue; // Skip self
|
||||
|
||||
|
|
@ -807,7 +818,7 @@ export async function inspectChains(
|
|||
if (neighborFile && "extension" in neighborFile && neighborFile.extension === "md") {
|
||||
const { edges: neighborEdges } = await buildNoteIndex(app, neighborFile as TFile);
|
||||
allEdges.push(...neighborEdges);
|
||||
console.log(`[Chain Inspector] Loaded ${neighborEdges.length} edges from ${neighborPath} (outgoing neighbor)`);
|
||||
getChainInspectorLogger().debug(`Loaded ${neighborEdges.length} edges from ${neighborPath} (outgoing neighbor)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -815,7 +826,9 @@ export async function inspectChains(
|
|||
// Deduplicate with outgoing neighbors (same file might be both incoming and outgoing)
|
||||
const allNeighborFiles = new Set<string>([...outgoingNeighborFiles]);
|
||||
|
||||
console.log(`[Chain Inspector] Loading ${notesLinkingToCurrent.size} notes that link to current note`);
|
||||
if (options.debugLogging) {
|
||||
getChainInspectorLogger().debug(`Loading ${notesLinkingToCurrent.size} notes that link to current note`);
|
||||
}
|
||||
for (const sourceFile of notesLinkingToCurrent) {
|
||||
if (sourceFile === context.file) continue; // Skip self
|
||||
if (allNeighborFiles.has(sourceFile)) continue; // Skip if already loaded as outgoing neighbor
|
||||
|
|
@ -824,16 +837,16 @@ export async function inspectChains(
|
|||
if (sourceNoteFile) {
|
||||
allNeighborFiles.add(sourceNoteFile.path);
|
||||
const { edges: sourceEdges } = await buildNoteIndex(app, sourceNoteFile);
|
||||
console.log(`[Chain Inspector] Loaded ${sourceEdges.length} edges from ${sourceFile}`);
|
||||
getChainInspectorLogger().debug(`Loaded ${sourceEdges.length} edges from ${sourceFile}`);
|
||||
|
||||
// Debug: Show all edges from this file (first 5) to understand what we're working with
|
||||
if (sourceEdges.length > 0) {
|
||||
console.log(`[Chain Inspector] Sample edges from ${sourceFile} (showing first 5):`);
|
||||
getChainInspectorLogger().debug(`Sample edges from ${sourceFile} (showing first 5):`);
|
||||
for (const edge of sourceEdges.slice(0, 5)) {
|
||||
const sourceInfo = "sectionHeading" in edge.source
|
||||
? `${edge.source.file}#${edge.source.sectionHeading || "null"}`
|
||||
: `${edge.source.file} (note-level)`;
|
||||
console.log(` - ${edge.rawEdgeType} from ${sourceInfo} -> ${edge.target.file}#${edge.target.heading || "null"}`);
|
||||
getChainInspectorLogger().debug(` - ${edge.rawEdgeType} from ${sourceInfo} -> ${edge.target.file}#${edge.target.heading || "null"}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -850,16 +863,16 @@ export async function inspectChains(
|
|||
return false;
|
||||
});
|
||||
if (edgesTargetingCurrentNote.length > 0) {
|
||||
console.log(`[Chain Inspector] ✓ Found ${edgesTargetingCurrentNote.length} edges from ${sourceFile} targeting current note (${context.file}):`);
|
||||
getChainInspectorLogger().debug(`✓ Found ${edgesTargetingCurrentNote.length} edges from ${sourceFile} targeting current note (${context.file}):`);
|
||||
for (const edge of edgesTargetingCurrentNote) {
|
||||
const sourceInfo = "sectionHeading" in edge.source
|
||||
? `${edge.source.file}#${edge.source.sectionHeading || "null"}`
|
||||
: `${edge.source.file} (note-level)`;
|
||||
console.log(` - ${edge.rawEdgeType} from ${sourceInfo} -> ${edge.target.file}#${edge.target.heading || "null"} [scope: ${edge.scope}]`);
|
||||
getChainInspectorLogger().debug(` - ${edge.rawEdgeType} from ${sourceInfo} -> ${edge.target.file}#${edge.target.heading || "null"} [scope: ${edge.scope}]`);
|
||||
}
|
||||
// Check why they don't match current section
|
||||
const currentSectionKey = `${context.file}:${context.heading || "null"}`;
|
||||
console.log(`[Chain Inspector] Current section: ${currentSectionKey}`);
|
||||
getChainInspectorLogger().debug(`Current section: ${currentSectionKey}`);
|
||||
// Use same matching logic as getNeighbors
|
||||
const debugFileBasename = context.file.split("/").pop()?.replace(/\.md$/, "") || "";
|
||||
const matchesCurrentFileDebug = (targetFile: string): boolean => {
|
||||
|
|
@ -874,18 +887,18 @@ export async function inspectChains(
|
|||
for (const edge of edgesTargetingCurrentNote) {
|
||||
const targetKey = `${edge.target.file}:${edge.target.heading || "null"}`;
|
||||
const fileMatches = matchesCurrentFileDebug(edge.target.file);
|
||||
const headingMatches = edge.target.heading === context.heading ||
|
||||
const headingMatches = headingsMatch(edge.target.heading, context.heading) ||
|
||||
(edge.target.heading === null && context.heading !== null);
|
||||
const matches = fileMatches && headingMatches;
|
||||
console.log(` - Edge target: ${targetKey}, file matches: ${fileMatches ? "YES" : "NO"}, heading matches: ${headingMatches ? "YES" : "NO"}, should match: ${matches ? "YES" : "NO"}`);
|
||||
getChainInspectorLogger().debug(` - Edge target: ${targetKey}, file matches: ${fileMatches ? "YES" : "NO"}, heading matches: ${headingMatches ? "YES" : "NO"}, should match: ${matches ? "YES" : "NO"}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[Chain Inspector] ✗ No edges from ${sourceFile} target current note (${context.file})`);
|
||||
console.log(` - Edges in this file target: ${[...new Set(sourceEdges.map(e => e.target.file))].slice(0, 3).join(", ")}...`);
|
||||
getChainInspectorLogger().debug(`✗ No edges from ${sourceFile} target current note (${context.file})`);
|
||||
getChainInspectorLogger().debug(` - Edges in this file target: ${[...new Set(sourceEdges.map(e => e.target.file))].slice(0, 3).join(", ")}...`);
|
||||
}
|
||||
allEdges.push(...sourceEdges);
|
||||
} else {
|
||||
console.log(`[Chain Inspector] Could not load neighbor note: ${sourceFile}`);
|
||||
getChainInspectorLogger().debug(`Could not load neighbor note: ${sourceFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -896,9 +909,9 @@ export async function inspectChains(
|
|||
sectionsWithContent[context.sectionIndex]?.content || "";
|
||||
|
||||
// Get neighbors (now includes edges from neighbor notes)
|
||||
console.log(`[Chain Inspector] Total edges after loading neighbors: ${allEdges.length} (current: ${currentEdges.length}, neighbors: ${allEdges.length - currentEdges.length})`);
|
||||
getChainInspectorLogger().debug(`Total edges after loading neighbors: ${allEdges.length} (current: ${currentEdges.length}, neighbors: ${allEdges.length - currentEdges.length})`);
|
||||
const neighbors = getNeighbors(allEdges, context, options);
|
||||
console.log(`[Chain Inspector] Neighbors found: ${neighbors.incoming.length} incoming, ${neighbors.outgoing.length} outgoing`);
|
||||
getChainInspectorLogger().debug(`Neighbors found: ${neighbors.incoming.length} incoming, ${neighbors.outgoing.length} outgoing`);
|
||||
|
||||
// Traverse paths (now includes edges from neighbor notes)
|
||||
const paths = traversePaths(allEdges, context, options);
|
||||
|
|
@ -910,7 +923,7 @@ export async function inspectChains(
|
|||
const vocabText = await VocabularyLoader.loadText(app, edgeVocabularyPath);
|
||||
edgeVocabulary = parseEdgeVocabulary(vocabText);
|
||||
} catch (error) {
|
||||
console.warn(`[Chain Inspector] Could not load edge vocabulary from ${edgeVocabularyPath}:`, error);
|
||||
getChainInspectorLogger().warn(`Could not load edge vocabulary from ${edgeVocabularyPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -920,7 +933,7 @@ export async function inspectChains(
|
|||
const effectiveIncomingCount = neighbors.incoming.length;
|
||||
const effectiveOutgoingCount = neighbors.outgoing.length;
|
||||
const effectiveFilteredEdges = filterEdges(allEdges, options);
|
||||
console.log(`[Chain Inspector] Before computeFindings: effectiveIncoming=${effectiveIncomingCount}, effectiveOutgoing=${effectiveOutgoingCount}, effectiveFilteredEdges=${effectiveFilteredEdges.length}`);
|
||||
getChainInspectorLogger().debug(`Before computeFindings: effectiveIncoming=${effectiveIncomingCount}, effectiveOutgoing=${effectiveOutgoingCount}, effectiveFilteredEdges=${effectiveFilteredEdges.length}`);
|
||||
|
||||
let findings = computeFindings(
|
||||
allEdges, // Use allEdges so we can detect incoming edges from neighbor notes
|
||||
|
|
@ -992,8 +1005,9 @@ export async function inspectChains(
|
|||
|
||||
// Log start-of-run header with resolved profile and settings
|
||||
const requiredLinks = profile?.required_links ?? false;
|
||||
console.log(
|
||||
`[Chain Inspector] Run: profile=${profileName} (resolvedFrom=${resolvedFrom}) required_links=${requiredLinks} includeCandidates=${options.includeCandidates} maxDepth=${options.maxDepth} direction=${options.direction}`
|
||||
|
||||
getChainInspectorLogger().info(
|
||||
`Run: profile=${profileName} (resolvedFrom=${resolvedFrom}) required_links=${requiredLinks} includeCandidates=${options.includeCandidates} maxDepth=${options.maxDepth} direction=${options.direction}`
|
||||
);
|
||||
|
||||
// Template matching
|
||||
|
|
@ -1052,9 +1066,24 @@ export async function inspectChains(
|
|||
return a.templateName.localeCompare(b.templateName);
|
||||
});
|
||||
|
||||
// Limit to topN (default: 3, configurable via options.maxTemplateMatches)
|
||||
// Limit to topN per template (default: 3), so up to N chains per chain type, not N total
|
||||
const topN = options.maxTemplateMatches ?? 3;
|
||||
templateMatches = sortedMatches.slice(0, topN);
|
||||
const byTemplate = new Map<string, typeof allTemplateMatches>();
|
||||
for (const m of sortedMatches) {
|
||||
const list = byTemplate.get(m.templateName) ?? [];
|
||||
list.push(m);
|
||||
byTemplate.set(m.templateName, list);
|
||||
}
|
||||
templateMatches = [];
|
||||
for (const list of byTemplate.values()) {
|
||||
templateMatches.push(...list.slice(0, topN));
|
||||
}
|
||||
templateMatches.sort((a, b) => {
|
||||
const rankDiff = confidenceRank(b.confidence) - confidenceRank(a.confidence);
|
||||
if (rankDiff !== 0) return rankDiff;
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return a.templateName.localeCompare(b.templateName);
|
||||
});
|
||||
|
||||
// Store topNUsed in analysisMeta
|
||||
if (analysisMeta) {
|
||||
|
|
@ -1128,7 +1157,7 @@ export async function inspectChains(
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Chain Inspector] Template matching failed:", e);
|
||||
getChainInspectorLogger().error("Template matching failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,15 +34,40 @@ export interface SectionNode {
|
|||
const NOTE_LINKS_HEADING = "Note-Verbindungen";
|
||||
const CANDIDATES_HEADING = "Kandidaten";
|
||||
|
||||
/** Intra-note block reference: [[#^block-id]] or [[#^block-id|alias]] */
|
||||
const INTRA_NOTE_BLOCK_REF_RE = /^#\^([a-zA-Z0-9_-]+)(?:\s*\|.*)?$/;
|
||||
|
||||
/**
|
||||
* Parse target from link text: [[file]] or [[file#Heading]]
|
||||
* Resolve [[#^block-id]] to (currentFile, sectionHeading) using section blockIds.
|
||||
* Returns null if not an intra-note block ref.
|
||||
*/
|
||||
function parseTarget(linkText: string): EdgeTarget {
|
||||
function resolveIntraNoteBlockRef(
|
||||
linkText: string,
|
||||
currentFilePath: string,
|
||||
sections: NoteSection[]
|
||||
): EdgeTarget | null {
|
||||
const trimmed = linkText.trim();
|
||||
const match = trimmed.match(INTRA_NOTE_BLOCK_REF_RE);
|
||||
if (!match) return null;
|
||||
const blockId = match[1];
|
||||
const section = sections.find((s) => s.blockId === blockId);
|
||||
return {
|
||||
file: currentFilePath,
|
||||
heading: section?.heading ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse target from link text: [[file]], [[file#Heading]], or [[#^block-id]] (intra-note).
|
||||
*/
|
||||
function parseTarget(linkText: string, currentFilePath: string, sections: NoteSection[]): EdgeTarget {
|
||||
const intraNote = resolveIntraNoteBlockRef(linkText, currentFilePath, sections);
|
||||
if (intraNote) return intraNote;
|
||||
|
||||
const normalized = normalizeLinkTarget(linkText);
|
||||
const parts = linkText.split("#");
|
||||
|
||||
|
||||
if (parts.length > 1) {
|
||||
// Has heading: [[file#Heading]]
|
||||
const file = normalized;
|
||||
const headingPart = parts[1]?.split("|")[0]?.trim();
|
||||
return {
|
||||
|
|
@ -50,8 +75,7 @@ function parseTarget(linkText: string): EdgeTarget {
|
|||
heading: headingPart || null,
|
||||
};
|
||||
}
|
||||
|
||||
// No heading: [[file]]
|
||||
|
||||
return {
|
||||
file: normalized,
|
||||
heading: null,
|
||||
|
|
@ -100,9 +124,9 @@ export async function buildNoteIndex(
|
|||
? { file: file.path }
|
||||
: { file: file.path, sectionHeading: section.heading };
|
||||
|
||||
// Process each target
|
||||
// Process each target (resolve [[#^block-id]] to same-note section)
|
||||
for (const targetLink of parsedEdge.targets) {
|
||||
const target = parseTarget(targetLink);
|
||||
const target = parseTarget(targetLink, file.path, sections);
|
||||
|
||||
edges.push({
|
||||
rawEdgeType: parsedEdge.rawType,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ import type { ChainRolesConfig } from "../dictionary/types";
|
|||
import type { IndexedEdge } from "./graphIndex";
|
||||
import type { EdgeVocabulary } from "../vocab/types";
|
||||
import { resolveCanonicalEdgeType } from "./chainInspector";
|
||||
import { splitIntoSections } from "../mapping/sectionParser";
|
||||
import { headingsMatch, normalizeHeadingForMatch } from "../unresolvedLink/linkHelpers";
|
||||
import { getLogger } from "../utils/logger";
|
||||
|
||||
// Don't create logger instance at import time - create it when needed
|
||||
function getTemplateMatchingLogger() {
|
||||
return getLogger("templateMatching");
|
||||
}
|
||||
|
||||
export interface NodeKey {
|
||||
file: string;
|
||||
|
|
@ -16,7 +24,10 @@ export interface NodeKey {
|
|||
|
||||
export interface CandidateNode {
|
||||
nodeKey: NodeKey;
|
||||
noteType: string; // "unknown" if not found
|
||||
/** Note-level type from frontmatter `type:`. */
|
||||
noteType: string;
|
||||
/** WP-26: Section-type or note-type used for slot matching: sectionType ?? noteType. */
|
||||
effectiveType: string;
|
||||
}
|
||||
|
||||
export interface TemplateMatch {
|
||||
|
|
@ -161,13 +172,16 @@ async function buildCandidateNodes(
|
|||
currentContext: { file: string; heading: string | null },
|
||||
allEdges: IndexedEdge[],
|
||||
options: { includeNoteLinks: boolean; includeCandidates: boolean },
|
||||
maxNodes: number = 30
|
||||
maxNodes: number = Number.MAX_SAFE_INTEGER // Remove limit: process all nodes to find all edges
|
||||
): Promise<CandidateNode[]> {
|
||||
const nodeKeys = new Set<string>();
|
||||
const nodeMap = new Map<string, CandidateNode>();
|
||||
|
||||
// Normalize heading so "Ergebnis & Auswirkung" and "Ergebnis & Auswirkung ^impact" become one key (one candidate per section)
|
||||
const norm = (h: string | null | undefined) => normalizeHeadingForMatch(h ?? null) ?? "";
|
||||
|
||||
// Add current context node
|
||||
const currentKey = `${currentContext.file}:${currentContext.heading || ""}`;
|
||||
const currentKey = `${currentContext.file}:${norm(currentContext.heading)}`;
|
||||
nodeKeys.add(currentKey);
|
||||
|
||||
// Add all target nodes from edges
|
||||
|
|
@ -175,13 +189,13 @@ async function buildCandidateNodes(
|
|||
if (edge.scope === "candidate" && !options.includeCandidates) continue;
|
||||
if (edge.scope === "note" && !options.includeNoteLinks) continue;
|
||||
|
||||
const targetKey = `${edge.target.file}:${edge.target.heading || ""}`;
|
||||
const targetKey = `${edge.target.file}:${norm(edge.target.heading)}`;
|
||||
nodeKeys.add(targetKey);
|
||||
|
||||
// Add source nodes (for incoming edges)
|
||||
// Note: Source files should already be full paths from graphIndex
|
||||
if ("sectionHeading" in edge.source) {
|
||||
const sourceKey = `${edge.source.file}:${edge.source.sectionHeading || ""}`;
|
||||
const sourceKey = `${edge.source.file}:${norm(edge.source.sectionHeading)}`;
|
||||
nodeKeys.add(sourceKey);
|
||||
} else {
|
||||
const sourceKey = `${edge.source.file}:`;
|
||||
|
|
@ -189,28 +203,37 @@ async function buildCandidateNodes(
|
|||
}
|
||||
}
|
||||
|
||||
// Limit to maxNodes (deterministic sorting)
|
||||
const sortedKeys = Array.from(nodeKeys).sort();
|
||||
const limitedKeys = sortedKeys.slice(0, maxNodes);
|
||||
// Prioritize nodes from current note (intra-note chains have highest probability)
|
||||
const currentFile = currentContext.file;
|
||||
const currentNoteKeys = Array.from(nodeKeys).filter((k) => {
|
||||
const i = k.indexOf(":");
|
||||
const file = i >= 0 ? k.slice(0, i) : k;
|
||||
return file === currentFile;
|
||||
}).sort();
|
||||
const otherKeys = Array.from(nodeKeys).filter((k) => {
|
||||
const i = k.indexOf(":");
|
||||
const file = i >= 0 ? k.slice(0, i) : k;
|
||||
return file !== currentFile;
|
||||
}).sort();
|
||||
const limitedKeys = [...currentNoteKeys, ...otherKeys].slice(0, maxNodes);
|
||||
|
||||
// Load note types for each node
|
||||
// Load note types and sections (for effective_type) per file
|
||||
const candidateNodes: CandidateNode[] = [];
|
||||
const fileCache = new Map<string, string>(); // resolved file path -> noteType
|
||||
const fileCache = new Map<string, string>(); // resolved path -> noteType
|
||||
const sectionsCache = new Map<string, ReturnType<typeof splitIntoSections>>(); // resolved path -> sections
|
||||
const pathResolutionCache = new Map<string, string | null>(); // original file -> resolved path
|
||||
|
||||
for (const key of limitedKeys) {
|
||||
const [file, heading] = key.split(":");
|
||||
const colonIndex = key.indexOf(":");
|
||||
const file = colonIndex >= 0 ? key.slice(0, colonIndex) : key;
|
||||
const heading = colonIndex >= 0 ? key.slice(colonIndex + 1) : "";
|
||||
if (!file) continue;
|
||||
|
||||
// Resolve file path (handle both full paths and link names)
|
||||
let resolvedPath = pathResolutionCache.get(file);
|
||||
if (resolvedPath === undefined) {
|
||||
// Try direct path first
|
||||
let fileObj = app.vault.getAbstractFileByPath(file);
|
||||
|
||||
// If not found and doesn't look like a full path, try resolving as link
|
||||
if (!fileObj && !file.includes("/") && !file.endsWith(".md")) {
|
||||
// Try to resolve as wikilink from current context
|
||||
const resolved = app.metadataCache.getFirstLinkpathDest(file, currentContext.file);
|
||||
if (resolved) {
|
||||
fileObj = resolved;
|
||||
|
|
@ -223,35 +246,85 @@ async function buildCandidateNodes(
|
|||
} else {
|
||||
resolvedPath = null;
|
||||
}
|
||||
|
||||
pathResolutionCache.set(file, resolvedPath);
|
||||
}
|
||||
|
||||
if (!resolvedPath) {
|
||||
// File not found, skip this node
|
||||
continue;
|
||||
}
|
||||
if (!resolvedPath) continue;
|
||||
|
||||
// Get note type (cache per resolved file path)
|
||||
// Load note type and sections (WP-26: sectionType for effective_type)
|
||||
let noteType = fileCache.get(resolvedPath);
|
||||
if (noteType === undefined) {
|
||||
let sections = sectionsCache.get(resolvedPath);
|
||||
if (noteType === undefined || sections === undefined) {
|
||||
try {
|
||||
const fileObj = app.vault.getAbstractFileByPath(resolvedPath);
|
||||
if (fileObj && "extension" in fileObj && fileObj.extension === "md") {
|
||||
const content = await app.vault.cachedRead(fileObj as TFile);
|
||||
noteType = extractNoteType(content);
|
||||
if (noteType === undefined) {
|
||||
noteType = extractNoteType(content);
|
||||
fileCache.set(resolvedPath, noteType);
|
||||
}
|
||||
if (sections === undefined) {
|
||||
sections = splitIntoSections(content);
|
||||
sectionsCache.set(resolvedPath, sections);
|
||||
}
|
||||
} else {
|
||||
noteType = "unknown";
|
||||
if (noteType === undefined) {
|
||||
noteType = "unknown";
|
||||
fileCache.set(resolvedPath, noteType);
|
||||
}
|
||||
if (sections === undefined) {
|
||||
sections = [];
|
||||
sectionsCache.set(resolvedPath, sections);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
noteType = "unknown";
|
||||
if (noteType === undefined) {
|
||||
noteType = "unknown";
|
||||
fileCache.set(resolvedPath, noteType);
|
||||
}
|
||||
if (sections === undefined) {
|
||||
sections = [];
|
||||
sectionsCache.set(resolvedPath, sections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// effective_type: sectionType ?? noteType (WP-26); use section's actual heading for display when matched
|
||||
const headingOrNull = heading || null;
|
||||
|
||||
// Rule: If this is a note-level node (heading === null), check if the note has multiple sections
|
||||
// with different types. If so, skip the note-level node - only individual sections should be candidates.
|
||||
if (!headingOrNull && sections.length > 0) {
|
||||
// Check if there are multiple sections with different effective types
|
||||
const sectionTypes = new Set<string>();
|
||||
for (const section of sections) {
|
||||
const sectionEffectiveType = section.sectionType || noteType;
|
||||
sectionTypes.add(sectionEffectiveType);
|
||||
}
|
||||
|
||||
// If there are multiple different types, skip note-level node
|
||||
if (sectionTypes.size > 1) {
|
||||
getTemplateMatchingLogger().debug(
|
||||
`Skipping note-level node for ${resolvedPath}: note has ${sectionTypes.size} different section types (${Array.from(sectionTypes).join(", ")})`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let effectiveType = noteType;
|
||||
let displayHeading: string | null = headingOrNull;
|
||||
if (headingOrNull && sections.length > 0) {
|
||||
const section = sections.find((s) => headingsMatch(s.heading, headingOrNull));
|
||||
if (section) {
|
||||
if (section.sectionType) effectiveType = section.sectionType;
|
||||
displayHeading = section.heading; // canonical heading from file (e.g. with ^block-id)
|
||||
}
|
||||
fileCache.set(resolvedPath, noteType);
|
||||
}
|
||||
|
||||
candidateNodes.push({
|
||||
nodeKey: { file: resolvedPath, heading: heading || null },
|
||||
nodeKey: { file: resolvedPath, heading: displayHeading },
|
||||
noteType,
|
||||
effectiveType,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -282,24 +355,34 @@ function getEdgeRole(
|
|||
|
||||
/**
|
||||
* Check if node matches slot constraints.
|
||||
* Strict: if allowed_node_types is defined, node must match exactly.
|
||||
* Uses effectiveType (sectionType ?? noteType) for WP-26 section-type-aware matching.
|
||||
*/
|
||||
function nodeMatchesSlot(
|
||||
node: CandidateNode,
|
||||
slot: ChainTemplateSlot
|
||||
): boolean {
|
||||
// If slot has allowed_node_types defined, enforce strict matching
|
||||
if (slot.allowed_node_types && slot.allowed_node_types.length > 0) {
|
||||
return slot.allowed_node_types.includes(node.noteType);
|
||||
return slot.allowed_node_types.includes(node.effectiveType);
|
||||
}
|
||||
|
||||
// No constraints = any type allowed (permissive behavior for backward compatibility)
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Parse node key "file:heading" (file may contain path with /). */
|
||||
function parseNodeKey(key: string): { file: string; heading: string | null } {
|
||||
const i = key.lastIndexOf(":");
|
||||
if (i < 0) return { file: key, heading: null };
|
||||
return { file: key.slice(0, i), heading: key.slice(i + 1) || null };
|
||||
}
|
||||
|
||||
/** Normalize path for comparison (vault paths: forward slashes, no trailing slash). */
|
||||
function normalizePathForComparison(path: string): string {
|
||||
return path.trim().replace(/\\/g, "/").replace(/\/+$/, "") || path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find edge between two nodes.
|
||||
* Uses resolved paths from candidate nodes and edge target resolution map.
|
||||
* Uses resolved paths and headingsMatch for heading comparison (block-id variants).
|
||||
* Paths are normalized so different slash styles (e.g. Windows) still match.
|
||||
*/
|
||||
function findEdgeBetween(
|
||||
fromKey: string,
|
||||
|
|
@ -310,37 +393,111 @@ function findEdgeBetween(
|
|||
edgeVocabulary: EdgeVocabulary | null,
|
||||
edgeTargetResolutionMap: Map<string, string>
|
||||
): { edgeRole: string | null; rawEdgeType: string } | null {
|
||||
const [fromFile, fromHeading] = fromKey.split(":");
|
||||
const [toFile, toHeading] = toKey.split(":");
|
||||
|
||||
const { file: fromFile, heading: fromHeading } = parseNodeKey(fromKey);
|
||||
const { file: toFile, heading: toHeading } = parseNodeKey(toKey);
|
||||
const fromFileNorm = normalizePathForComparison(fromFile);
|
||||
const toFileNorm = normalizePathForComparison(toFile);
|
||||
|
||||
for (const edge of allEdges) {
|
||||
// Resolve source file path
|
||||
const sourceFile = "sectionHeading" in edge.source ? edge.source.file : edge.source.file;
|
||||
const resolvedSourceFile = sourceFile.includes("/") || sourceFile.endsWith(".md")
|
||||
? sourceFile
|
||||
: edgeTargetResolutionMap.get(sourceFile) || sourceFile;
|
||||
|
||||
// Resolve target file path
|
||||
const resolvedTargetFile = edgeTargetResolutionMap.get(edge.target.file) || edge.target.file;
|
||||
|
||||
const edgeFromKey = "sectionHeading" in edge.source
|
||||
? `${resolvedSourceFile}:${edge.source.sectionHeading || ""}`
|
||||
: `${resolvedSourceFile}:`;
|
||||
const edgeToKey = `${resolvedTargetFile}:${edge.target.heading || ""}`;
|
||||
|
||||
// Match keys (handle null heading as empty string)
|
||||
if (edgeFromKey === fromKey && edgeToKey === toKey) {
|
||||
const resolvedSourceNorm = normalizePathForComparison(resolvedSourceFile);
|
||||
const resolvedTargetNorm = normalizePathForComparison(resolvedTargetFile);
|
||||
|
||||
const sourceHeading = "sectionHeading" in edge.source ? edge.source.sectionHeading : null;
|
||||
const targetHeading = edge.target.heading;
|
||||
|
||||
if (
|
||||
resolvedSourceNorm === fromFileNorm &&
|
||||
headingsMatch(sourceHeading, fromHeading) &&
|
||||
resolvedTargetNorm === toFileNorm &&
|
||||
headingsMatch(targetHeading, toHeading)
|
||||
) {
|
||||
const canonical = canonicalEdgeType(edge.rawEdgeType);
|
||||
const edgeRole = getEdgeRole(edge.rawEdgeType, canonical, chainRoles);
|
||||
return { edgeRole, rawEdgeType: edge.rawEdgeType };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostic: when a link is not satisfied, log why (for learning→behavior / section_edges debugging).
|
||||
*/
|
||||
function logLinkResolutionDiagnostic(
|
||||
link: { from: string; to: string },
|
||||
fromKey: string,
|
||||
toKey: string,
|
||||
allEdges: IndexedEdge[],
|
||||
edgeTargetResolutionMap: Map<string, string>,
|
||||
chainRoles: ChainRolesConfig | null
|
||||
): void {
|
||||
// Only log for specific links we're debugging to avoid flooding console
|
||||
// Focus on learning->behavior or other critical links
|
||||
const isDebugLink = (link.from === "learning" && link.to === "behavior") ||
|
||||
(link.from === "experience" && link.to === "insight") ||
|
||||
(link.from === "insight" && link.to === "decision");
|
||||
|
||||
if (!isDebugLink) {
|
||||
return; // Skip logging for other links to reduce noise
|
||||
}
|
||||
|
||||
const { file: fromFile } = parseNodeKey(fromKey);
|
||||
const { file: toFile, heading: toHeading } = parseNodeKey(toKey);
|
||||
const fromFileNorm = normalizePathForComparison(fromFile);
|
||||
const toFileNorm = normalizePathForComparison(toFile);
|
||||
|
||||
const candidates = allEdges.filter((e) => {
|
||||
const sf = "sectionHeading" in e.source ? e.source.file : e.source.file;
|
||||
const resolved = sf.includes("/") || sf.endsWith(".md") ? sf : edgeTargetResolutionMap.get(sf) || sf;
|
||||
return normalizePathForComparison(resolved) === fromFileNorm;
|
||||
});
|
||||
|
||||
getTemplateMatchingLogger().debug(`link_resolution: ${link.from}→${link.to} fromKey=${fromKey} toKey=${toKey} candidates_from_same_file=${candidates.length}`);
|
||||
// Limit to first 5 candidates instead of 10 to reduce log size
|
||||
for (const e of candidates.slice(0, 5)) {
|
||||
const srcH = "sectionHeading" in e.source ? e.source.sectionHeading : null;
|
||||
const resT = edgeTargetResolutionMap.get(e.target.file) || e.target.file;
|
||||
const resTNorm = normalizePathForComparison(resT);
|
||||
const fileMatch = resTNorm === toFileNorm;
|
||||
const srcMatch = headingsMatch(srcH, parseNodeKey(fromKey).heading);
|
||||
const tgtMatch = headingsMatch(e.target.heading, toHeading);
|
||||
getTemplateMatchingLogger().debug(` - ${e.rawEdgeType} sourceHeading=${JSON.stringify(srcH)} targetFile=${e.target.file} targetHeading=${JSON.stringify(e.target.heading)} | srcMatch=${srcMatch} tgtFileMatch=${fileMatch} tgtHeadingMatch=${tgtMatch}`);
|
||||
}
|
||||
const withMatchingTarget = candidates.filter((e) => {
|
||||
const resT = edgeTargetResolutionMap.get(e.target.file) || e.target.file;
|
||||
return (
|
||||
normalizePathForComparison(resT) === toFileNorm &&
|
||||
headingsMatch(e.target.heading, toHeading) &&
|
||||
headingsMatch("sectionHeading" in e.source ? e.source.sectionHeading : null, parseNodeKey(fromKey).heading)
|
||||
);
|
||||
});
|
||||
if (withMatchingTarget.length > 0 && chainRoles) {
|
||||
for (const e of withMatchingTarget) {
|
||||
const role = getEdgeRole(e.rawEdgeType, undefined, chainRoles);
|
||||
getTemplateMatchingLogger().debug(` -> edge ${e.rawEdgeType} would match link but getEdgeRole=${role ?? "null"} (check chain_roles for this type)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer role from rawEdgeType by looking up in chainRoles (fallback when getEdgeRole returns null).
|
||||
*/
|
||||
function inferRoleFromRawType(rawEdgeType: string, chainRoles: ChainRolesConfig | null): string | null {
|
||||
if (!chainRoles) return null;
|
||||
for (const [roleName, role] of Object.entries(chainRoles.roles)) {
|
||||
if (role.edge_types?.includes(rawEdgeType)) return roleName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Score assignment for template matching.
|
||||
* Boosts assignments where all nodes are in the same note (intra-note chains have highest probability).
|
||||
*/
|
||||
function scoreAssignment(
|
||||
template: ChainTemplate,
|
||||
|
|
@ -351,7 +508,9 @@ function scoreAssignment(
|
|||
chainRoles: ChainRolesConfig | null,
|
||||
edgeVocabulary: EdgeVocabulary | null,
|
||||
profile?: TemplateMatchingProfile,
|
||||
edgeTargetResolutionMap?: Map<string, string>
|
||||
edgeTargetResolutionMap?: Map<string, string>,
|
||||
currentContextFile?: string,
|
||||
debugLogging?: boolean
|
||||
): {
|
||||
score: number;
|
||||
satisfiedLinks: number;
|
||||
|
|
@ -362,14 +521,22 @@ function scoreAssignment(
|
|||
let satisfiedLinks = 0;
|
||||
let requiredLinks = normalized.links.length;
|
||||
const roleEvidence: Array<{ from: string; to: string; edgeRole: string; rawEdgeType: string }> = [];
|
||||
|
||||
|
||||
// Determine required_links: profile > template.matching > defaults.matching
|
||||
const requiredLinksEnabled = profile?.required_links ??
|
||||
template.matching?.required_links ??
|
||||
const requiredLinksEnabled = profile?.required_links ??
|
||||
template.matching?.required_links ??
|
||||
false;
|
||||
|
||||
|
||||
// Score slots filled
|
||||
score += assignment.size * 2;
|
||||
|
||||
// Small intra-note bonus so intra-note is preferred but cross-note can still reach top K (was +15, cross-note never made it)
|
||||
if (currentContextFile && assignment.size > 0) {
|
||||
const allSameFile = [...assignment.values()].every((n) => n.nodeKey.file === currentContextFile);
|
||||
if (allSameFile) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Score links
|
||||
for (const link of normalized.links) {
|
||||
|
|
@ -395,43 +562,78 @@ function scoreAssignment(
|
|||
edgeVocabulary,
|
||||
edgeTargetResolutionMap || new Map()
|
||||
);
|
||||
|
||||
const effectiveRole = edge?.edgeRole ?? (edge ? inferRoleFromRawType(edge.rawEdgeType, chainRoles) : null);
|
||||
|
||||
if (edge && edge.edgeRole) {
|
||||
// Check if edge role is allowed
|
||||
getTemplateMatchingLogger().debugObject(`[scoreAssignment] Link ${link.from}→${link.to}`, {
|
||||
fromKey,
|
||||
toKey,
|
||||
edgeFound: !!edge,
|
||||
edgeType: edge?.rawEdgeType,
|
||||
edgeRole: edge?.edgeRole,
|
||||
effectiveRole,
|
||||
allowedEdgeRoles: link.allowed_edge_roles,
|
||||
matches: edge && effectiveRole && (!link.allowed_edge_roles || link.allowed_edge_roles.length === 0 || link.allowed_edge_roles.includes(effectiveRole))
|
||||
});
|
||||
|
||||
if (edge && effectiveRole) {
|
||||
if (!link.allowed_edge_roles || link.allowed_edge_roles.length === 0) {
|
||||
// No constraints = any role allowed
|
||||
score += 10;
|
||||
satisfiedLinks++;
|
||||
roleEvidence.push({
|
||||
from: link.from,
|
||||
to: link.to,
|
||||
edgeRole: edge.edgeRole,
|
||||
edgeRole: effectiveRole,
|
||||
rawEdgeType: edge.rawEdgeType,
|
||||
});
|
||||
} else if (link.allowed_edge_roles.includes(edge.edgeRole)) {
|
||||
} else if (link.allowed_edge_roles.includes(effectiveRole)) {
|
||||
score += 10;
|
||||
satisfiedLinks++;
|
||||
roleEvidence.push({
|
||||
from: link.from,
|
||||
to: link.to,
|
||||
edgeRole: edge.edgeRole,
|
||||
edgeRole: effectiveRole,
|
||||
rawEdgeType: edge.rawEdgeType,
|
||||
});
|
||||
} else if (requiredLinksEnabled) {
|
||||
score -= 5; // Penalty for wrong role on required link
|
||||
score -= 5;
|
||||
}
|
||||
} else {
|
||||
if (requiredLinksEnabled) {
|
||||
score -= 5;
|
||||
}
|
||||
if (debugLogging) {
|
||||
logLinkResolutionDiagnostic(
|
||||
link,
|
||||
fromKey,
|
||||
toKey,
|
||||
allEdges,
|
||||
edgeTargetResolutionMap || new Map(),
|
||||
chainRoles
|
||||
);
|
||||
}
|
||||
} else if (requiredLinksEnabled) {
|
||||
score -= 5; // Penalty for missing required link
|
||||
}
|
||||
}
|
||||
|
||||
return { score, satisfiedLinks, requiredLinks, roleEvidence };
|
||||
}
|
||||
|
||||
const MAX_ASSIGNMENTS_COLLECTED = 80;
|
||||
|
||||
/** Two assignments are distinct if at least one slot points to a different node. Uses normalized heading so "Feedback" and "Feedback ^block-id" count as the same section. */
|
||||
function assignmentSignature(slots: ChainTemplateSlot[], assignment: Map<string, CandidateNode>): string {
|
||||
const parts = slots.map((s) => {
|
||||
const node = assignment.get(s.id);
|
||||
const headingNorm = node ? (normalizeHeadingForMatch(node.nodeKey.heading) ?? "") : "";
|
||||
return node ? `${s.id}:${node.nodeKey.file}:${headingNorm}` : `${s.id}:`;
|
||||
});
|
||||
return parts.sort().join("|");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find best assignment for a template using backtracking.
|
||||
* Find top K distinct assignments for a template (e.g. intra-note and cross-note loop_learning).
|
||||
*/
|
||||
function findBestAssignment(
|
||||
function findTopKAssignments(
|
||||
template: ChainTemplate,
|
||||
normalized: { slots: ChainTemplateSlot[]; links: ChainTemplateLink[] },
|
||||
candidateNodes: CandidateNode[],
|
||||
|
|
@ -441,25 +643,34 @@ function findBestAssignment(
|
|||
edgeVocabulary: EdgeVocabulary | null,
|
||||
profile?: TemplateMatchingProfile,
|
||||
edgeTargetResolutionMap?: Map<string, string>,
|
||||
templatesConfig?: ChainTemplatesConfig | null
|
||||
): TemplateMatch | null {
|
||||
templatesConfig?: ChainTemplatesConfig | null,
|
||||
currentContextFile?: string,
|
||||
topK: number = 2,
|
||||
debugLogging?: boolean
|
||||
): TemplateMatch[] {
|
||||
const slots = normalized.slots;
|
||||
if (slots.length === 0) return null;
|
||||
|
||||
// Filter candidates per slot
|
||||
if (slots.length === 0) return [];
|
||||
|
||||
const slotCandidates = new Map<string, CandidateNode[]>();
|
||||
for (const slot of slots) {
|
||||
const candidates = candidateNodes.filter((node) => nodeMatchesSlot(node, slot));
|
||||
slotCandidates.set(slot.id, candidates);
|
||||
}
|
||||
|
||||
// Backtracking assignment
|
||||
let bestMatch: TemplateMatch | null = null;
|
||||
let bestScore = -Infinity;
|
||||
|
||||
|
||||
if (debugLogging) {
|
||||
const slotSummary = slots.map((s) => {
|
||||
const cands = slotCandidates.get(s.id) || [];
|
||||
const files = [...new Set(cands.map((c) => c.nodeKey.file))].slice(0, 5);
|
||||
return `${s.id}=${cands.length}(${files.map((f) => f.split("/").pop() || f).join(",")}${cands.length > 5 ? "…" : ""})`;
|
||||
}).join("; ");
|
||||
getTemplateMatchingLogger().debug(`Template ${template.name}: slot candidates: ${slotSummary}`);
|
||||
}
|
||||
|
||||
type Collected = { assignment: Map<string, CandidateNode>; score: number; result: ReturnType<typeof scoreAssignment> };
|
||||
const collected: Collected[] = [];
|
||||
|
||||
function backtrack(assignment: Map<string, CandidateNode>, slotIndex: number) {
|
||||
if (slotIndex >= slots.length) {
|
||||
// Evaluate assignment
|
||||
const result = scoreAssignment(
|
||||
template,
|
||||
normalized,
|
||||
|
|
@ -469,78 +680,150 @@ function findBestAssignment(
|
|||
chainRoles,
|
||||
edgeVocabulary,
|
||||
profile,
|
||||
edgeTargetResolutionMap
|
||||
edgeTargetResolutionMap,
|
||||
currentContextFile,
|
||||
debugLogging
|
||||
);
|
||||
|
||||
if (result.score > bestScore) {
|
||||
bestScore = result.score;
|
||||
const slotAssignments: TemplateMatch["slotAssignments"] = {};
|
||||
const missingSlots: string[] = [];
|
||||
|
||||
for (const slot of slots) {
|
||||
const node = assignment.get(slot.id);
|
||||
if (node) {
|
||||
slotAssignments[slot.id] = {
|
||||
nodeKey: `${node.nodeKey.file}:${node.nodeKey.heading || ""}`,
|
||||
file: node.nodeKey.file,
|
||||
heading: node.nodeKey.heading,
|
||||
noteType: node.noteType,
|
||||
};
|
||||
} else {
|
||||
missingSlots.push(slot.id);
|
||||
}
|
||||
if (collected.length < MAX_ASSIGNMENTS_COLLECTED) {
|
||||
collected.push({ assignment: new Map(assignment), score: result.score, result });
|
||||
} else {
|
||||
const minIdx = collected.reduce((i, c, j) => (c.score < (collected[i]?.score ?? c.score) ? j : i), 0);
|
||||
const worst = collected[minIdx];
|
||||
if (worst != null && result.score > worst.score) {
|
||||
collected[minIdx] = { assignment: new Map(assignment), score: result.score, result };
|
||||
}
|
||||
|
||||
// Calculate completeness and confidence (will be set after matching)
|
||||
bestMatch = {
|
||||
templateName: template.name,
|
||||
score: result.score,
|
||||
slotAssignments,
|
||||
missingSlots,
|
||||
satisfiedLinks: result.satisfiedLinks,
|
||||
requiredLinks: result.requiredLinks,
|
||||
roleEvidence: result.roleEvidence.length > 0 ? result.roleEvidence : undefined,
|
||||
slotsComplete: false, // Will be set later
|
||||
linksComplete: false, // Will be set later
|
||||
confidence: "weak", // Will be set later
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const slot = slots[slotIndex];
|
||||
if (!slot) return;
|
||||
|
||||
|
||||
const candidates = slotCandidates.get(slot.id) || [];
|
||||
|
||||
// Try each candidate
|
||||
|
||||
for (const candidate of candidates) {
|
||||
// Check if already assigned (no duplicates)
|
||||
let alreadyAssigned = false;
|
||||
for (const assignedNode of assignment.values()) {
|
||||
if (
|
||||
assignedNode.nodeKey.file === candidate.nodeKey.file &&
|
||||
assignedNode.nodeKey.heading === candidate.nodeKey.heading
|
||||
headingsMatch(assignedNode.nodeKey.heading, candidate.nodeKey.heading)
|
||||
) {
|
||||
alreadyAssigned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!alreadyAssigned) {
|
||||
assignment.set(slot.id, candidate);
|
||||
backtrack(assignment, slotIndex + 1);
|
||||
assignment.delete(slot.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Also try leaving slot unassigned
|
||||
|
||||
backtrack(assignment, slotIndex + 1);
|
||||
}
|
||||
|
||||
|
||||
backtrack(new Map(), 0);
|
||||
|
||||
return bestMatch;
|
||||
|
||||
collected.sort((a, b) => b.score - a.score);
|
||||
|
||||
const completeCount = collected.filter((c) => {
|
||||
let missing = 0;
|
||||
for (const slot of slots) {
|
||||
if (!c.assignment.get(slot.id)) missing++;
|
||||
}
|
||||
return missing === 0;
|
||||
}).length;
|
||||
|
||||
getTemplateMatchingLogger().debug(`Template ${template.name}: collected=${collected.length}, complete=${completeCount}, will return up to ${topK}`);
|
||||
|
||||
const shortFile = (path: string) => path.split("/").pop() || path;
|
||||
const out: TemplateMatch[] = [];
|
||||
const seenSignatures = new Set<string>();
|
||||
|
||||
function pushMatch(
|
||||
assignment: Map<string, CandidateNode>,
|
||||
score: number,
|
||||
result: ReturnType<typeof scoreAssignment>
|
||||
): void {
|
||||
const slotAssignments: TemplateMatch["slotAssignments"] = {};
|
||||
const missingSlots: string[] = [];
|
||||
|
||||
for (const slot of slots) {
|
||||
const node = assignment.get(slot.id);
|
||||
if (node) {
|
||||
slotAssignments[slot.id] = {
|
||||
nodeKey: `${node.nodeKey.file}:${node.nodeKey.heading || ""}`,
|
||||
file: node.nodeKey.file,
|
||||
heading: node.nodeKey.heading,
|
||||
noteType: node.effectiveType,
|
||||
};
|
||||
} else {
|
||||
missingSlots.push(slot.id);
|
||||
}
|
||||
}
|
||||
|
||||
const sig = assignmentSignature(slots, assignment);
|
||||
if (seenSignatures.has(sig)) return;
|
||||
seenSignatures.add(sig);
|
||||
|
||||
out.push({
|
||||
templateName: template.name,
|
||||
score,
|
||||
slotAssignments,
|
||||
missingSlots,
|
||||
satisfiedLinks: result.satisfiedLinks,
|
||||
requiredLinks: result.requiredLinks,
|
||||
roleEvidence: result.roleEvidence.length > 0 ? result.roleEvidence : undefined,
|
||||
slotsComplete: false,
|
||||
linksComplete: false,
|
||||
confidence: "weak",
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 1: add complete assignments (all slots filled) up to topK
|
||||
for (const { assignment, score, result } of collected) {
|
||||
if (out.length >= topK) break;
|
||||
const missingCount = slots.filter((s) => !assignment.get(s.id)).length;
|
||||
if (missingCount > 0) continue;
|
||||
pushMatch(assignment, score, result);
|
||||
}
|
||||
|
||||
// Phase 2: add incomplete assignments (some slots missing) until topK – chains are found even when not all slots can be filled
|
||||
for (const { assignment, score, result } of collected) {
|
||||
if (out.length >= topK) break;
|
||||
const missingCount = slots.filter((s) => !assignment.get(s.id)).length;
|
||||
if (missingCount === 0) continue;
|
||||
pushMatch(assignment, score, result);
|
||||
}
|
||||
|
||||
if (out.length > 0) {
|
||||
getTemplateMatchingLogger().debug(`--- Template ${template.name}: ${out.length} match(es) returned (these go into the report) ---`);
|
||||
let crossNoteCount = 0;
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
const m = out[i];
|
||||
if (!m) continue;
|
||||
const isCrossNote = currentContextFile
|
||||
? Object.values(m.slotAssignments).some((s) => s.file !== currentContextFile)
|
||||
: false;
|
||||
if (isCrossNote) crossNoteCount++;
|
||||
getTemplateMatchingLogger().debug(` #${i + 1} score=${m.score} links=${m.satisfiedLinks}/${m.requiredLinks}${isCrossNote ? " [cross-note]" : ""}`);
|
||||
for (const slot of slots) {
|
||||
const s = m.slotAssignments[slot.id];
|
||||
if (s) {
|
||||
const f = shortFile(s.file);
|
||||
const rawH = s.heading ?? "note";
|
||||
const h = rawH.length > 40 ? rawH.slice(0, 37) + "…" : rawH;
|
||||
getTemplateMatchingLogger().debug(` ${slot.id}: ${f}#${h}`);
|
||||
} else {
|
||||
getTemplateMatchingLogger().debug(` ${slot.id}: —`);
|
||||
}
|
||||
}
|
||||
}
|
||||
getTemplateMatchingLogger().debug(` Diversity: ${crossNoteCount} of ${out.length} match(es) use node(s) from another note (cross-note).`);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -579,7 +862,7 @@ export async function matchTemplates(
|
|||
templatesConfig: ChainTemplatesConfig | null,
|
||||
chainRoles: ChainRolesConfig | null,
|
||||
edgeVocabulary: EdgeVocabulary | null,
|
||||
options: { includeNoteLinks: boolean; includeCandidates: boolean },
|
||||
options: { includeNoteLinks: boolean; includeCandidates: boolean; maxMatchesPerTemplateDefault?: number; debugLogging?: boolean },
|
||||
profile?: TemplateMatchingProfile
|
||||
): Promise<TemplateMatch[]> {
|
||||
if (!templatesConfig || !templatesConfig.templates || templatesConfig.templates.length === 0) {
|
||||
|
|
@ -587,12 +870,13 @@ export async function matchTemplates(
|
|||
}
|
||||
|
||||
// Build candidate nodes (with resolved paths)
|
||||
// No limit on nodes: process all nodes to ensure all edges are considered
|
||||
const candidateNodes = await buildCandidateNodes(
|
||||
app,
|
||||
currentContext,
|
||||
allEdges,
|
||||
options,
|
||||
30
|
||||
Number.MAX_SAFE_INTEGER
|
||||
);
|
||||
|
||||
// Create a map from original edge target to resolved path for edge matching
|
||||
|
|
@ -609,13 +893,18 @@ export async function matchTemplates(
|
|||
return result.canonical;
|
||||
};
|
||||
|
||||
// Match each template
|
||||
// Match each template; distinct assignments per template (e.g. intra-note + cross-note).
|
||||
// Use the greater of YAML and plugin setting so plugin "2" is not overridden by YAML "1".
|
||||
const yamlMax = templatesConfig.defaults?.matching?.max_matches_per_template ?? 0;
|
||||
const pluginMax = options.maxMatchesPerTemplateDefault ?? 2;
|
||||
const maxPerTemplate = Math.max(1, Math.min(10, Math.max(yamlMax, pluginMax)));
|
||||
|
||||
const matches: TemplateMatch[] = [];
|
||||
|
||||
|
||||
for (const template of templatesConfig.templates) {
|
||||
const normalized = normalizeTemplate(template, templatesConfig);
|
||||
|
||||
const match = findBestAssignment(
|
||||
|
||||
const templateMatches = findTopKAssignments(
|
||||
template,
|
||||
normalized,
|
||||
candidateNodes,
|
||||
|
|
@ -625,13 +914,24 @@ export async function matchTemplates(
|
|||
edgeVocabulary,
|
||||
profile,
|
||||
edgeTargetResolutionMap,
|
||||
templatesConfig
|
||||
templatesConfig,
|
||||
currentContext.file,
|
||||
maxPerTemplate,
|
||||
options.debugLogging
|
||||
);
|
||||
|
||||
if (match) {
|
||||
|
||||
for (const match of templateMatches) {
|
||||
matches.push(match);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.debugLogging) {
|
||||
const byName: Record<string, number> = {};
|
||||
for (const m of matches) {
|
||||
byName[m.templateName] = (byName[m.templateName] ?? 0) + 1;
|
||||
}
|
||||
getTemplateMatchingLogger().info(`Template matching done: total=${matches.length} per_template=${JSON.stringify(byName)}`);
|
||||
}
|
||||
|
||||
// Calculate completeness and confidence for each match
|
||||
// Get causal-ish roles from config or use default
|
||||
|
|
@ -656,7 +956,7 @@ export async function matchTemplates(
|
|||
match.confidence = "weak";
|
||||
} else {
|
||||
// Rule 2: slotsComplete=true
|
||||
const hasCausalRole = match.roleEvidence?.some((ev) => causalIshRoles.includes(ev.edgeRole)) ?? false;
|
||||
const hasCausalRole = match.roleEvidence?.some((ev: { edgeRole: string }) => causalIshRoles.includes(ev.edgeRole)) ?? false;
|
||||
|
||||
// If linksComplete=true AND there is at least one causal-ish role evidence => "confirmed"
|
||||
if (match.linksComplete && hasCausalRole) {
|
||||
|
|
@ -669,13 +969,15 @@ export async function matchTemplates(
|
|||
}
|
||||
|
||||
// Sort by score desc, then name asc (deterministic)
|
||||
matches.sort((a, b) => {
|
||||
matches.sort((a: TemplateMatch, b: TemplateMatch) => {
|
||||
if (b.score !== a.score) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
return a.templateName.localeCompare(b.templateName);
|
||||
});
|
||||
|
||||
// Keep top K (K=1 for v0)
|
||||
return matches.slice(0, 1);
|
||||
// Return all matches; chainInspector applies topN (maxTemplateMatches). Multiple chains
|
||||
// per note/section are supported (multiple templates); multiple matches per same template
|
||||
// would require findTopKAssignments per template (future).
|
||||
return matches;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export async function executeChainWorkbench(
|
|||
maxDepth: 3,
|
||||
direction: "both" as const,
|
||||
maxTemplateMatches: undefined, // No limit - we want ALL matches
|
||||
maxMatchesPerTemplateDefault: settings.maxMatchesPerTemplateDefault,
|
||||
debugLogging: settings.debugLogging,
|
||||
};
|
||||
|
||||
// Load vocabulary
|
||||
|
|
@ -89,7 +91,8 @@ export async function executeChainWorkbench(
|
|||
chainTemplates,
|
||||
chainRoles,
|
||||
edgeVocabulary,
|
||||
allEdges
|
||||
allEdges,
|
||||
settings.debugLogging
|
||||
);
|
||||
|
||||
// Open workbench UI
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export async function executeFixFindings(
|
|||
includeCandidates: false,
|
||||
maxDepth: 3,
|
||||
direction: "both",
|
||||
maxMatchesPerTemplateDefault: settings.maxMatchesPerTemplateDefault,
|
||||
debugLogging: settings.debugLogging,
|
||||
};
|
||||
|
||||
const report = await inspectChains(
|
||||
|
|
|
|||
|
|
@ -223,6 +223,8 @@ export async function executeInspectChains(
|
|||
maxDepth: options.maxDepth ?? 3,
|
||||
direction: options.direction ?? "both",
|
||||
maxTemplateMatches: options.maxTemplateMatches ?? settings.chainInspectorMaxTemplateMatches,
|
||||
maxMatchesPerTemplateDefault: settings.maxMatchesPerTemplateDefault,
|
||||
debugLogging: settings.debugLogging,
|
||||
};
|
||||
|
||||
// Prepare templates source info
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export function parseChainTemplates(yamlText: string): ParseChainTemplatesResult
|
|||
if (obj.defaults && typeof obj.defaults === "object" && !Array.isArray(obj.defaults)) {
|
||||
const defaults = obj.defaults as Record<string, unknown>;
|
||||
config.defaults = {
|
||||
matching: defaults.matching as { required_links?: boolean } | undefined,
|
||||
matching: defaults.matching as { required_links?: boolean; max_matches_per_template?: number } | undefined,
|
||||
profiles: defaults.profiles as {
|
||||
discovery?: import("./types").TemplateMatchingProfile;
|
||||
decisioning?: import("./types").TemplateMatchingProfile;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export interface ChainTemplatesConfig {
|
|||
defaults?: {
|
||||
matching?: {
|
||||
required_links?: boolean;
|
||||
/** Max distinct assignments per template (e.g. 2 = intra-note + cross-note). Default 2. */
|
||||
max_matches_per_template?: number;
|
||||
};
|
||||
profiles?: {
|
||||
discovery?: TemplateMatchingProfile;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface RenderOptions {
|
|||
vocabulary?: Vocabulary | null;
|
||||
noteType?: string; // Fallback für effective_type
|
||||
sectionEdgeTypes?: Map<string, Map<string, string>>; // fromBlockId -> (toBlockId -> edgeType) für manuell geänderte Edge-Types
|
||||
noteEdges?: Map<string, Map<string, string>>; // fromBlockId (oder "ROOT") -> (toNote oder toNote#Abschnitt -> edgeType)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -289,6 +290,11 @@ function renderCaptureText(
|
|||
// Diese werden später in einem zweiten Durchlauf hinzugefügt
|
||||
const backwardEdges = ""; // Wird später hinzugefügt
|
||||
|
||||
// WP-26: Note-Edges (Link zu anderer Note oder Note#Abschnitt) für diese Section
|
||||
const noteEdgesStr = sectionInfo?.blockId
|
||||
? renderNoteEdges(sectionInfo.blockId, context)
|
||||
: "";
|
||||
|
||||
// Use template if provided
|
||||
if (step.output?.template) {
|
||||
const templateText = renderTemplate(step.output.template, {
|
||||
|
|
@ -298,12 +304,12 @@ function renderCaptureText(
|
|||
});
|
||||
|
||||
// WP-26: Template mit Heading, Section-Type und Edges kombinieren
|
||||
return combineSectionParts(heading, sectionTypeCallout, templateText, references, forwardEdges, backwardEdges);
|
||||
return combineSectionParts(heading, sectionTypeCallout, templateText, references, forwardEdges, backwardEdges, noteEdgesStr);
|
||||
}
|
||||
|
||||
// Default: use section if provided, otherwise just the text
|
||||
if (step.section) {
|
||||
return combineSectionParts(heading, sectionTypeCallout, text, references, forwardEdges, backwardEdges);
|
||||
return combineSectionParts(heading, sectionTypeCallout, text, references, forwardEdges, backwardEdges, noteEdgesStr);
|
||||
}
|
||||
|
||||
return text;
|
||||
|
|
@ -410,8 +416,8 @@ function renderCaptureTextLine(
|
|||
|
||||
// WP-26: Template mit Heading, Section-Type und Edges kombinieren (wenn Heading vorhanden)
|
||||
if (headingPrefix) {
|
||||
// Backward-Edges werden später im zweiten Durchlauf hinzugefügt
|
||||
return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, "");
|
||||
const noteEdgesStr = sectionInfo?.blockId ? renderNoteEdges(sectionInfo.blockId, context) : "";
|
||||
return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, "", noteEdgesStr);
|
||||
}
|
||||
|
||||
return templateText;
|
||||
|
|
@ -419,8 +425,8 @@ function renderCaptureTextLine(
|
|||
|
||||
// WP-26: Wenn Heading vorhanden, mit Section-Type und Edges kombinieren
|
||||
if (headingPrefix) {
|
||||
// Backward-Edges werden später im zweiten Durchlauf hinzugefügt
|
||||
return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, "");
|
||||
const noteEdgesStr = sectionInfo?.blockId ? renderNoteEdges(sectionInfo.blockId, context) : "";
|
||||
return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, "", noteEdgesStr);
|
||||
}
|
||||
|
||||
// Default: text with heading prefix if configured
|
||||
|
|
@ -672,7 +678,8 @@ function combineSectionParts(
|
|||
content: string,
|
||||
references: string,
|
||||
autoEdges: string,
|
||||
backwardEdges: string = ""
|
||||
backwardEdges: string = "",
|
||||
noteEdges: string = ""
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
|
|
@ -688,8 +695,8 @@ function combineSectionParts(
|
|||
parts.push(content);
|
||||
}
|
||||
|
||||
// WP-26: Alle Edges im abstract wrapper am Ende der Sektion
|
||||
const allEdges = [references, autoEdges, backwardEdges].filter(e => e.trim());
|
||||
// WP-26: Alle Edges im abstract wrapper am Ende der Sektion (inkl. Note-Edges mit [[Note#Abschnitt]])
|
||||
const allEdges = [references, autoEdges, backwardEdges, noteEdges].filter(e => e.trim());
|
||||
if (allEdges.length > 0) {
|
||||
const abstractWrapper = buildAbstractWrapper(allEdges.join("\n"));
|
||||
if (abstractWrapper) {
|
||||
|
|
@ -723,8 +730,8 @@ function buildAbstractWrapper(edgesContent: string): string | null {
|
|||
edgeGroups.set(currentEdgeType, []);
|
||||
}
|
||||
} else {
|
||||
// Prüfe auf Link: > [[#^block-id]]
|
||||
const linkMatch = line.match(/^>\s*(\[\[#\^.+?\]\])$/);
|
||||
// Prüfe auf Link: > [[#^block-id]] oder > [[Note]] / > [[Note#Abschnitt]]
|
||||
const linkMatch = line.match(/^>\s*(\[\[[^\]]+?\]\])$/);
|
||||
if (linkMatch && linkMatch[1] && currentEdgeType) {
|
||||
const links = edgeGroups.get(currentEdgeType) || [];
|
||||
links.push(linkMatch[1]);
|
||||
|
|
@ -832,6 +839,26 @@ function renderReferences(
|
|||
return edgeCallouts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Edge-Callouts für Note-Verbindungen ([[Note]] oder [[Note#Abschnitt]]).
|
||||
* Format: > [!edge] <edge_type>\n> [[toNote]] bzw. > [[toNote#Abschnitt]]
|
||||
*/
|
||||
function renderNoteEdges(blockId: string, context: RenderContext): string {
|
||||
const noteEdges = context.options.noteEdges;
|
||||
if (!noteEdges) return "";
|
||||
|
||||
const fromBlock = noteEdges.get(blockId);
|
||||
if (!fromBlock || fromBlock.size === 0) return "";
|
||||
|
||||
const edgeCallouts: string[] = [];
|
||||
for (const [toNote, edgeType] of fromBlock.entries()) {
|
||||
if (!toNote || !edgeType) continue;
|
||||
edgeCallouts.push(`> [!edge] ${edgeType}`);
|
||||
edgeCallouts.push(`> [[${toNote}]]`);
|
||||
}
|
||||
return edgeCallouts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt den effektiven Section-Type basierend auf Heading-Level.
|
||||
* Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section
|
||||
|
|
|
|||
51
src/main.ts
51
src/main.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { Notice, Plugin, TFile } from "obsidian";
|
||||
import { App, Notice, Plugin, TFile } from "obsidian";
|
||||
import { DEFAULT_SETTINGS, type MindnetSettings, normalizeVaultPath } from "./settings";
|
||||
import { VocabularyLoader } from "./vocab/VocabularyLoader";
|
||||
import { parseEdgeVocabulary } from "./vocab/parseEdgeVocabulary";
|
||||
|
|
@ -78,6 +78,24 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
|||
// Add settings tab
|
||||
this.addSettingTab(new MindnetSettingTab(this.app, this));
|
||||
|
||||
this.addCommand({
|
||||
id: "mindnet-open-settings",
|
||||
name: "Mindnet: Einstellungen öffnen",
|
||||
callback: () => {
|
||||
const app = this.app as App & { setting?: { open: () => void; openTabById?: (id: string) => void } };
|
||||
if (app.setting?.open) {
|
||||
app.setting.open();
|
||||
if (typeof app.setting.openTabById === "function") {
|
||||
app.setting.openTabById(this.manifest.id);
|
||||
} else {
|
||||
new Notice("In der linken Leiste auf „Mindnet Causal Assistant“ klicken.");
|
||||
}
|
||||
} else {
|
||||
new Notice("Einstellungen öffnen → Community-Plugins → Mindnet Causal Assistant.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Register unresolved link handlers for Reading View and Live Preview
|
||||
if (this.settings.interceptUnresolvedLinkClicks) {
|
||||
this.registerUnresolvedLinkHandlers();
|
||||
|
|
@ -567,6 +585,14 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
|||
const chainTemplates = this.chainTemplates.data;
|
||||
const templatesLoadResult = this.chainTemplates.result;
|
||||
|
||||
if (!chainRoles || !chainTemplates) {
|
||||
const missing: string[] = [];
|
||||
if (!chainRoles) missing.push("Chain Roles");
|
||||
if (!chainTemplates) missing.push("Chain Templates");
|
||||
new Notice(`${missing.join(" und ")} nicht geladen. Bitte Pfad in Einstellungen prüfen (z. B. ${this.settings.chainRolesPath}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeInspectChains(
|
||||
this.app,
|
||||
editor,
|
||||
|
|
@ -610,6 +636,14 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
|||
const chainTemplates = this.chainTemplates.data;
|
||||
const templatesLoadResult = this.chainTemplates.result;
|
||||
|
||||
if (!chainRoles || !chainTemplates) {
|
||||
const missing: string[] = [];
|
||||
if (!chainRoles) missing.push("Chain Roles");
|
||||
if (!chainTemplates) missing.push("Chain Templates");
|
||||
new Notice(`${missing.join(" und ")} nicht geladen. Bitte Pfad in Einstellungen prüfen (z. B. ${this.settings.chainRolesPath}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { executeChainWorkbench } = await import("./commands/chainWorkbenchCommand");
|
||||
await executeChainWorkbench(
|
||||
this.app,
|
||||
|
|
@ -641,6 +675,14 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
|||
const chainTemplates = this.chainTemplates.data;
|
||||
const templatesLoadResult = this.chainTemplates.result;
|
||||
|
||||
if (!chainRoles || !chainTemplates) {
|
||||
const missing: string[] = [];
|
||||
if (!chainRoles) missing.push("Chain Roles");
|
||||
if (!chainTemplates) missing.push("Chain Templates");
|
||||
new Notice(`${missing.join(" und ")} nicht geladen. Bitte Pfad in Einstellungen prüfen (z. B. ${this.settings.chainRolesPath}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { executeVaultTriageScan } = await import("./commands/vaultTriageScanCommand");
|
||||
await executeVaultTriageScan(
|
||||
this.app,
|
||||
|
|
@ -1224,6 +1266,13 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
|||
|
||||
private async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
|
||||
// Initialize module-specific logging
|
||||
const { initializeLogging } = await import("./utils/logger");
|
||||
if (!this.settings.moduleLogLevels) {
|
||||
this.settings.moduleLogLevels = {};
|
||||
}
|
||||
initializeLogging(this.settings.moduleLogLevels);
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,13 @@ export function extractExistingMappings(
|
|||
}
|
||||
}
|
||||
|
||||
// Detect wrapper block
|
||||
const wrapperBlock = detectWrapperBlock(lines, wrapperCalloutType, wrapperTitle);
|
||||
|
||||
// Detect wrapper block (strict: callout type + title match)
|
||||
let wrapperBlock = detectWrapperBlock(lines, wrapperCalloutType, wrapperTitle);
|
||||
// Fallback: find any [!abstract] block so we can insert into it even if title differs
|
||||
if (!wrapperBlock) {
|
||||
wrapperBlock = detectAbstractBlockPermissive(lines);
|
||||
}
|
||||
|
||||
return {
|
||||
existingMappings,
|
||||
wrapperBlockStartLine: wrapperBlock?.startLine ?? null,
|
||||
|
|
@ -137,6 +141,54 @@ function detectWrapperBlock(
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissive: find first block that starts with > [!abstract] (any title).
|
||||
* Use when strict detectWrapperBlock failed (e.g. different title in note).
|
||||
*/
|
||||
function detectAbstractBlockPermissive(lines: string[]): WrapperBlockLocation | null {
|
||||
const abstractHeaderRe = /^\s*>\s*\[!abstract\]\s*[+-]?\s*.+$/i;
|
||||
let wrapperStart: number | null = null;
|
||||
let wrapperEnd: number | null = null;
|
||||
let quoteLevel = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line === undefined) continue;
|
||||
|
||||
if (abstractHeaderRe.test(line)) {
|
||||
wrapperStart = i;
|
||||
quoteLevel = (line.match(/^\s*(>+)/)?.[1]?.length || 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wrapperStart !== null && wrapperEnd === null) {
|
||||
const trimmed = line.trim();
|
||||
const currentQuoteLevel = (line.match(/^\s*(>+)/)?.[1]?.length || 0);
|
||||
|
||||
if (trimmed.match(/^\^map-/)) {
|
||||
wrapperEnd = i + 1;
|
||||
break;
|
||||
}
|
||||
if (trimmed !== "" && currentQuoteLevel < quoteLevel) {
|
||||
wrapperEnd = i;
|
||||
break;
|
||||
}
|
||||
if (!line.startsWith(">") && trimmed.match(/^#{1,6}\s+/)) {
|
||||
wrapperEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wrapperStart !== null && wrapperEnd === null) {
|
||||
wrapperEnd = lines.length;
|
||||
}
|
||||
if (wrapperStart !== null && wrapperEnd !== null) {
|
||||
return { startLine: wrapperStart, endLine: wrapperEnd };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove wrapper block from section content.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* Section parser: Split markdown by headings and extract wikilinks per section.
|
||||
*/
|
||||
|
||||
import { normalizeLinkTarget } from "../unresolvedLink/linkHelpers";
|
||||
|
||||
export interface NoteSection {
|
||||
heading: string | null; // null for content before first heading
|
||||
headingLevel: number; // 0 for content before first heading
|
||||
|
|
@ -11,8 +9,17 @@ export interface NoteSection {
|
|||
startLine: number; // Line index where section starts
|
||||
endLine: number; // Line index where section ends (exclusive)
|
||||
links: string[]; // Deduplicated wikilinks found in this section
|
||||
/** WP-26: Section type from `> [!section] type` callout in this section; null if not set. */
|
||||
sectionType: string | null;
|
||||
/** WP-26: Block ID from heading line (e.g. `## Title ^block-id`); null if not set. */
|
||||
blockId: string | null;
|
||||
}
|
||||
|
||||
/** Match `> [!section] type` callout (type = word characters). */
|
||||
const SECTION_CALLOUT_REGEX = /^\s*>\s*\[!section\]\s*(\S+)\s*$/i;
|
||||
/** Match block ID at end of heading text: `... ^block-id`. */
|
||||
const BLOCK_ID_IN_HEADING_REGEX = /\s+\^([a-zA-Z0-9_-]+)\s*$/;
|
||||
|
||||
/**
|
||||
* Split markdown content into sections by headings.
|
||||
*/
|
||||
|
|
@ -36,33 +43,43 @@ export function splitIntoSections(markdown: string): NoteSection[] {
|
|||
if (currentSection !== null || currentContent.length > 0) {
|
||||
const content = currentContent.join("\n");
|
||||
const links = extractWikilinks(content);
|
||||
|
||||
sections.push({
|
||||
heading: currentSection?.heading || null,
|
||||
headingLevel: currentSection?.headingLevel || 0,
|
||||
content: content,
|
||||
content,
|
||||
startLine: currentStartLine,
|
||||
endLine: i,
|
||||
links: links,
|
||||
links,
|
||||
sectionType: currentSection?.sectionType ?? null,
|
||||
blockId: currentSection?.blockId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// Start new section
|
||||
|
||||
// Start new section: extract blockId from heading text (e.g. "Title ^my-id")
|
||||
const headingLevel = (headingMatch[1]?.length || 0);
|
||||
const headingText = (headingMatch[2]?.trim() || "");
|
||||
const blockIdMatch = headingText.match(BLOCK_ID_IN_HEADING_REGEX);
|
||||
const blockId = blockIdMatch ? blockIdMatch[1] ?? null : null;
|
||||
currentSection = {
|
||||
heading: headingText,
|
||||
headingLevel: headingLevel,
|
||||
headingLevel,
|
||||
content: line,
|
||||
startLine: i,
|
||||
endLine: i + 1,
|
||||
links: [],
|
||||
sectionType: null,
|
||||
blockId,
|
||||
};
|
||||
currentContent = [line];
|
||||
currentStartLine = i;
|
||||
} else {
|
||||
// Add line to current section
|
||||
if (currentSection) {
|
||||
// WP-26: Detect `> [!section] type` callout (typically right after heading)
|
||||
const sectionCalloutMatch = line.match(SECTION_CALLOUT_REGEX);
|
||||
if (sectionCalloutMatch && sectionCalloutMatch[1]) {
|
||||
currentSection.sectionType = sectionCalloutMatch[1].trim();
|
||||
}
|
||||
currentContent.push(line);
|
||||
currentSection.content = currentContent.join("\n");
|
||||
currentSection.endLine = i + 1;
|
||||
|
|
@ -77,14 +94,15 @@ export function splitIntoSections(markdown: string): NoteSection[] {
|
|||
if (currentSection || currentContent.length > 0) {
|
||||
const content = currentContent.join("\n");
|
||||
const links = extractWikilinks(content);
|
||||
|
||||
sections.push({
|
||||
heading: currentSection?.heading || null,
|
||||
headingLevel: currentSection?.headingLevel || 0,
|
||||
content: content,
|
||||
content,
|
||||
startLine: currentStartLine,
|
||||
endLine: lines.length,
|
||||
links: links,
|
||||
links,
|
||||
sectionType: currentSection?.sectionType ?? null,
|
||||
blockId: currentSection?.blockId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -111,19 +129,19 @@ export function extractWikilinks(markdown: string): string[] {
|
|||
// It's [[rel:type|link]] format, extract the link part
|
||||
const linkPart = parts[1] || "";
|
||||
if (linkPart) {
|
||||
// Normalize: remove alias (|alias) and heading (#heading)
|
||||
const normalized = normalizeLinkTarget(linkPart);
|
||||
if (normalized) {
|
||||
links.add(normalized);
|
||||
// Alias entfernen (|), Abschnitt (#) behalten für Edge-Block
|
||||
const linkTarget = linkPart.split("|")[0]?.trim() || linkPart;
|
||||
if (linkTarget) {
|
||||
links.add(linkTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal link format [[link]]
|
||||
// Normalize: remove alias (|alias) and heading (#heading)
|
||||
const normalized = normalizeLinkTarget(target);
|
||||
if (normalized) {
|
||||
links.add(normalized);
|
||||
// Normal link format [[link]] oder [[link#Abschnitt]]
|
||||
// Alias entfernen (|), Abschnitt (#) behalten für Edge-Block
|
||||
const linkTarget = target.split("|")[0]?.trim() || target;
|
||||
if (linkTarget) {
|
||||
links.add(linkTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,12 +98,16 @@ export async function buildSemanticMappings(
|
|||
// Split into sections
|
||||
const sections = splitIntoSections(content);
|
||||
|
||||
// Build pending assignments map by section key (from rel: links)
|
||||
// rel: links are already converted, so we use the extracted mappings
|
||||
// Pending assignments nach Section-Key (linkBasename = voller Link inkl. #Abschnitt)
|
||||
const pendingBySection = new Map<string, Map<string, string>>(); // sectionKey -> (linkBasename -> edgeType)
|
||||
if (options?.pendingAssignments?.length) {
|
||||
for (const a of options.pendingAssignments) {
|
||||
const key = a.sectionKey;
|
||||
if (!pendingBySection.has(key)) pendingBySection.set(key, new Map());
|
||||
pendingBySection.get(key)!.set(a.linkBasename, a.chosenRawType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add rel: link mappings to pendingBySection (all go to ROOT for now, will be refined per section)
|
||||
// We'll match them to sections when processing each section
|
||||
const globalRelMappings = relLinkMappings;
|
||||
|
||||
const result: BuildResult = {
|
||||
|
|
@ -121,8 +125,12 @@ export async function buildSemanticMappings(
|
|||
for (const section of sections) {
|
||||
result.sectionsProcessed++;
|
||||
|
||||
if (section.links.length === 0) {
|
||||
// No links, skip
|
||||
const sectionKey = section.heading
|
||||
? `H${section.headingLevel}:${section.heading}`
|
||||
: "ROOT";
|
||||
const pendingForSection = pendingBySection.get(sectionKey);
|
||||
|
||||
if (section.links.length === 0 && !pendingForSection?.size) {
|
||||
modifiedSections.push({
|
||||
section,
|
||||
newContent: section.content,
|
||||
|
|
@ -139,30 +147,19 @@ export async function buildSemanticMappings(
|
|||
settings.mappingWrapperTitle
|
||||
);
|
||||
|
||||
// Merge pending assignments into existing mappings BEFORE building worklist
|
||||
// This ensures pending assignments are available when worklist is built
|
||||
// Determine section key for this section
|
||||
// Note: section.heading is string | null, headingLevel is number
|
||||
const sectionKey = section.heading
|
||||
? `H${section.headingLevel}:${section.heading}`
|
||||
: "ROOT";
|
||||
|
||||
// Merge rel: link mappings (from [[rel:type|link]] format) into existing mappings
|
||||
// These take precedence over file content mappings
|
||||
// Key = Section-Link (mit #Abschnitt falls vorhanden), damit Edge-Block >> [[Note#Abschnitt]] ausgibt
|
||||
for (const [linkBasename, edgeType] of globalRelMappings.entries()) {
|
||||
// Check if this link exists in this section
|
||||
const normalizedBasename = linkBasename.split("|")[0]?.split("#")[0]?.trim() || linkBasename;
|
||||
if (section.links.some(link => {
|
||||
const normalizedLink = link.split("|")[0]?.split("#")[0]?.trim() || link;
|
||||
return normalizedLink === normalizedBasename || link === normalizedBasename;
|
||||
})) {
|
||||
mappingState.existingMappings.set(normalizedBasename, edgeType);
|
||||
console.log(`[Mindnet] Merged rel: link mapping into existing mappings: ${normalizedBasename} -> ${edgeType}`);
|
||||
const matchingLink = section.links.find(
|
||||
link => link === linkBasename || (link.split("|")[0]?.trim() === linkBasename.split("|")[0]?.trim())
|
||||
);
|
||||
if (matchingLink !== undefined) {
|
||||
mappingState.existingMappings.set(matchingLink, edgeType);
|
||||
console.log(`[Mindnet] Merged rel: link mapping into existing mappings: ${matchingLink} -> ${edgeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Also merge legacy pending assignments if any (for backward compatibility)
|
||||
const pendingForSection = pendingBySection.get(sectionKey);
|
||||
// Pending-Assignments (z. B. aus Interview-Übersicht) mit vollem Link inkl. #Abschnitt
|
||||
if (pendingForSection) {
|
||||
for (const [linkBasename, edgeType] of pendingForSection.entries()) {
|
||||
mappingState.existingMappings.set(linkBasename, edgeType);
|
||||
|
|
@ -269,11 +266,14 @@ export async function buildSemanticMappings(
|
|||
// Always include existing mappings (preserve aliases as they are)
|
||||
for (const [link, edgeType] of mappingState.existingMappings.entries()) {
|
||||
if (section.links.includes(link)) {
|
||||
// Use existing type as-is (could be alias or canonical)
|
||||
mappingsToUse.set(link, edgeType);
|
||||
result.existingMappingsKept++;
|
||||
}
|
||||
}
|
||||
// Pending-Assignments (vollständiger Link inkl. #Abschnitt) übernehmen
|
||||
for (const [linkBasename, edgeType] of pendingForSection?.entries() ?? []) {
|
||||
mappingsToUse.set(linkBasename, edgeType);
|
||||
}
|
||||
|
||||
// Handle unmapped based on mode
|
||||
for (const link of section.links) {
|
||||
|
|
@ -292,6 +292,12 @@ export async function buildSemanticMappings(
|
|||
}
|
||||
}
|
||||
|
||||
// Links für Mapping-Block: Section-Links + Pending-Links (mit #Abschnitt)
|
||||
const linksForBlock = [...section.links];
|
||||
for (const k of pendingForSection?.keys() ?? []) {
|
||||
if (!linksForBlock.includes(k)) linksForBlock.push(k);
|
||||
}
|
||||
|
||||
// Build new mapping block
|
||||
const builderOptions: MappingBuilderOptions = {
|
||||
wrapperCalloutType: settings.mappingWrapperCalloutType,
|
||||
|
|
@ -301,8 +307,7 @@ export async function buildSemanticMappings(
|
|||
assignUnmapped: "none", // Already handled above, so set to "none" to avoid double-processing
|
||||
};
|
||||
|
||||
// Build mapping block
|
||||
const mappingBlock = buildMappingBlock(section.links, mappingsToUse, builderOptions);
|
||||
const mappingBlock = buildMappingBlock(linksForBlock, mappingsToUse, builderOptions);
|
||||
|
||||
if (mappingBlock) {
|
||||
result.sectionsWithMappings++;
|
||||
|
|
|
|||
|
|
@ -66,17 +66,14 @@ export async function updateMappingBlocksForRelLinks(
|
|||
);
|
||||
|
||||
// Merge rel: link mappings (from [[rel:type|link]] format) into existing mappings
|
||||
// These take precedence over file content mappings (update even if already mapped)
|
||||
// Key = Section-Link (mit #Abschnitt), damit Edge-Block >> [[Note#Abschnitt]] ausgibt
|
||||
for (const [linkBasename, edgeType] of relLinkMappings.entries()) {
|
||||
// Check if this link exists in this section
|
||||
const normalizedBasename = linkBasename.split("|")[0]?.split("#")[0]?.trim() || linkBasename;
|
||||
if (section.links.some(link => {
|
||||
const normalizedLink = link.split("|")[0]?.split("#")[0]?.trim() || link;
|
||||
return normalizedLink === normalizedBasename || link === normalizedBasename;
|
||||
})) {
|
||||
// Always update rel: link mappings (they represent user's explicit choice)
|
||||
mappingState.existingMappings.set(normalizedBasename, edgeType);
|
||||
console.log(`[UpdateMappingBlocks] Updated rel: link mapping: ${normalizedBasename} -> ${edgeType}`);
|
||||
const matchingLink = section.links.find(
|
||||
link => link === linkBasename || (link.split("|")[0]?.trim() === linkBasename.split("|")[0]?.trim())
|
||||
);
|
||||
if (matchingLink !== undefined) {
|
||||
mappingState.existingMappings.set(matchingLink, edgeType);
|
||||
console.log(`[UpdateMappingBlocks] Updated rel: link mapping: ${matchingLink} -> ${edgeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ export function extractRelLinks(content: string): RelLink[] {
|
|||
}
|
||||
|
||||
if (edgeType && linkPart) {
|
||||
// Extract basename (remove alias and heading)
|
||||
const basename = linkPart.split("|")[0]?.split("#")[0]?.trim() || linkPart;
|
||||
// Link-Ziel: Alias entfernen (|), Abschnitt (#) behalten für Edge-Block und Text-Link
|
||||
const linkTarget = linkPart.split("|")[0]?.trim() || linkPart;
|
||||
|
||||
relLinks.push({
|
||||
edgeType,
|
||||
linkBasename: basename,
|
||||
linkBasename: linkTarget,
|
||||
fullMatch: match[0],
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + match[0].length,
|
||||
|
|
@ -65,16 +65,15 @@ export function convertRelLinksToEdges(content: string): {
|
|||
const relLink = relLinks[i];
|
||||
if (!relLink) continue;
|
||||
|
||||
// Replace [[rel:type|link]] with [[link]]
|
||||
// Replace [[rel:type|link]] with [[link]] (link kann Note oder Note#Abschnitt sein)
|
||||
const normalLink = `[[${relLink.linkBasename}]]`;
|
||||
convertedContent =
|
||||
convertedContent.substring(0, relLink.startIndex) +
|
||||
normalLink +
|
||||
convertedContent.substring(relLink.endIndex);
|
||||
|
||||
// Store mapping (normalize basename)
|
||||
const normalizedBasename = relLink.linkBasename.split("|")[0]?.split("#")[0]?.trim() || relLink.linkBasename;
|
||||
edgeMappings.set(normalizedBasename, relLink.edgeType);
|
||||
// Mapping mit vollem Link als Key (Abschnitt bleibt erhalten)
|
||||
edgeMappings.set(relLink.linkBasename, relLink.edgeType);
|
||||
}
|
||||
|
||||
return { convertedContent, edgeMappings };
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export interface MindnetSettings {
|
|||
// Chain Inspector settings
|
||||
chainInspectorIncludeCandidates: boolean; // default: false
|
||||
chainInspectorMaxTemplateMatches: number; // default: 3
|
||||
/** Default max distinct matches per template (e.g. intra-note + cross-note). Overridable by chain_templates.yaml defaults.matching.max_matches_per_template. */
|
||||
maxMatchesPerTemplateDefault: number; // default: 2
|
||||
// Fix Actions settings
|
||||
fixActions: {
|
||||
createMissingNote: {
|
||||
|
|
@ -53,6 +55,14 @@ export interface MindnetSettings {
|
|||
keepOriginal: boolean;
|
||||
};
|
||||
};
|
||||
// Backend Logging Configuration
|
||||
logLevel: "DEBUG" | "INFO" | "WARNING" | "ERROR"; // Default: "INFO"
|
||||
logMaxBytes: number; // Default: 1048576 (1 MB)
|
||||
logBackupCount: number; // Default: 2
|
||||
// Module-specific logging configuration
|
||||
moduleLogLevels: {
|
||||
[moduleName: string]: "DEBUG" | "INFO" | "WARN" | "ERROR" | "NONE";
|
||||
}; // Default: {} (all modules use INFO)
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MindnetSettings = {
|
||||
|
|
@ -92,6 +102,7 @@ export interface MindnetSettings {
|
|||
templateMatchingProfile: "discovery",
|
||||
chainInspectorIncludeCandidates: false,
|
||||
chainInspectorMaxTemplateMatches: 3,
|
||||
maxMatchesPerTemplateDefault: 2,
|
||||
fixActions: {
|
||||
createMissingNote: {
|
||||
mode: "skeleton_only",
|
||||
|
|
@ -105,6 +116,15 @@ export interface MindnetSettings {
|
|||
keepOriginal: true,
|
||||
},
|
||||
},
|
||||
// Backend Logging Configuration
|
||||
logLevel: "INFO",
|
||||
logMaxBytes: 1048576, // 1 MB
|
||||
logBackupCount: 2,
|
||||
// Module-specific logging configuration
|
||||
moduleLogLevels: {
|
||||
// Default: all modules use INFO level
|
||||
// Example: "templateMatching": "DEBUG", "chainInspector": "WARN"
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -91,8 +91,14 @@ describe("Chain Inspector top-N template matches", () => {
|
|||
return; // Skip ordering tests if no matches
|
||||
}
|
||||
|
||||
// Should have at most 3 matches
|
||||
expect(report.templateMatches.length).toBeLessThanOrEqual(3);
|
||||
// Should have at most 3 matches per template (topN per chain type)
|
||||
const byTemplate = new Map<string, number>();
|
||||
for (const m of report.templateMatches) {
|
||||
byTemplate.set(m.templateName, (byTemplate.get(m.templateName) ?? 0) + 1);
|
||||
}
|
||||
for (const count of byTemplate.values()) {
|
||||
expect(count).toBeLessThanOrEqual(3);
|
||||
}
|
||||
|
||||
// Should have topNUsed in analysisMeta
|
||||
expect(report.analysisMeta?.topNUsed).toBe(3);
|
||||
|
|
|
|||
30
src/tests/analysis/graphIndex.intraNoteBlockRef.test.ts
Normal file
30
src/tests/analysis/graphIndex.intraNoteBlockRef.test.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Tests that [[#^block-id]] (intra-note block references) are resolved to (currentFile, sectionHeading).
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildNoteIndex } from "../../analysis/graphIndex";
|
||||
import { createVaultAppFromFixtures } from "../helpers/vaultHelper";
|
||||
import type { TFile } from "obsidian";
|
||||
|
||||
describe("graphIndex intra-note block ref [[#^block-id]]", () => {
|
||||
it("should resolve [[#^situation]] to current file and section heading with blockId situation", async () => {
|
||||
const app = createVaultAppFromFixtures();
|
||||
const file = app.vault.getAbstractFileByPath("Tests/intra_note_block_ref.md");
|
||||
if (!file || !("extension" in file) || file.extension !== "md") {
|
||||
throw new Error("Fixture Tests/intra_note_block_ref.md not found");
|
||||
}
|
||||
|
||||
const { edges } = await buildNoteIndex(app, file as TFile);
|
||||
|
||||
const beherrschtVon = edges.filter((e) => e.rawEdgeType === "beherrscht_von");
|
||||
expect(beherrschtVon.length).toBe(1);
|
||||
expect(beherrschtVon[0]?.target.file).toBe("Tests/intra_note_block_ref.md");
|
||||
expect(beherrschtVon[0]?.target.heading).toBe("Situation (Was ist passiert?) ^situation");
|
||||
|
||||
const impacts = edges.filter((e) => e.rawEdgeType === "impacts");
|
||||
expect(impacts.length).toBe(1);
|
||||
expect(impacts[0]?.target.file).toBe("Tests/intra_note_block_ref.md");
|
||||
expect(impacts[0]?.target.heading).toBe("Nächster Schritt ^next");
|
||||
});
|
||||
});
|
||||
121
src/tests/analysis/templateMatching.effectiveType.test.ts
Normal file
121
src/tests/analysis/templateMatching.effectiveType.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Tests for effectiveType (WP-26: sectionType ?? noteType) in template matching.
|
||||
* Verifies that Section-Type from [!section] callout overrides Note-Type from frontmatter.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { matchTemplates } from "../../analysis/templateMatching";
|
||||
import { createVaultAppFromFixtures } from "../helpers/vaultHelper";
|
||||
import { loadChainRolesFromFixtures, loadChainTemplatesFromFixtures } from "../helpers/configHelper";
|
||||
import { buildNoteIndex } from "../../analysis/graphIndex";
|
||||
import type { TFile } from "obsidian";
|
||||
import type { IndexedEdge } from "../../analysis/graphIndex";
|
||||
import type { EdgeVocabulary } from "../../vocab/types";
|
||||
|
||||
describe("template matching effectiveType (sectionType ?? noteType)", () => {
|
||||
const mockEdgeVocabulary: EdgeVocabulary = {
|
||||
byCanonical: new Map(),
|
||||
aliasToCanonical: new Map(),
|
||||
};
|
||||
|
||||
it("should use sectionType as effectiveType when section has [!section] callout (overrides note type)", async () => {
|
||||
const app = createVaultAppFromFixtures();
|
||||
const chainRoles = loadChainRolesFromFixtures();
|
||||
const chainTemplates = loadChainTemplatesFromFixtures();
|
||||
|
||||
if (!chainRoles || !chainTemplates) {
|
||||
console.warn("Skipping test: real config files not found in fixtures");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fixture 06_section_type_override.md has type: concept but section "Kontext" has > [!section] experience
|
||||
const file06 = app.vault.getAbstractFileByPath("Tests/06_section_type_override.md");
|
||||
if (!file06 || !("extension" in file06) || file06.extension !== "md") {
|
||||
throw new Error("Fixture Tests/06_section_type_override.md not found");
|
||||
}
|
||||
|
||||
const { edges: edges06 } = await buildNoteIndex(app, file06 as TFile);
|
||||
const allEdges: IndexedEdge[] = [...edges06];
|
||||
|
||||
const file03 = app.vault.getAbstractFileByPath("Tests/03_insight_transformation.md");
|
||||
if (file03 && "extension" in file03 && file03.extension === "md") {
|
||||
const { edges: edges03 } = await buildNoteIndex(app, file03 as TFile);
|
||||
allEdges.push(...edges03);
|
||||
}
|
||||
const file04 = app.vault.getAbstractFileByPath("Tests/04_decision_outcome.md");
|
||||
if (file04 && "extension" in file04 && file04.extension === "md") {
|
||||
const { edges: edges04 } = await buildNoteIndex(app, file04 as TFile);
|
||||
allEdges.push(...edges04);
|
||||
}
|
||||
|
||||
const matches = await matchTemplates(
|
||||
app,
|
||||
{ file: "Tests/06_section_type_override.md", heading: "Kontext" },
|
||||
allEdges,
|
||||
chainTemplates,
|
||||
chainRoles,
|
||||
mockEdgeVocabulary,
|
||||
{ includeNoteLinks: true, includeCandidates: false }
|
||||
);
|
||||
|
||||
expect(matches.length).toBeGreaterThan(0);
|
||||
const match = matches.find((m) => m.templateName === "trigger_transformation_outcome");
|
||||
expect(match).toBeDefined();
|
||||
|
||||
// Current section (06#Kontext) has [!section] experience → effectiveType = "experience"
|
||||
// So it must be assignable to trigger slot (allowed_node_types includes experience)
|
||||
// and reported noteType in slotAssignments must be "experience", not "concept"
|
||||
const triggerAssignment = match?.slotAssignments?.trigger;
|
||||
expect(triggerAssignment).toBeDefined();
|
||||
expect(triggerAssignment?.file).toContain("06_section_type_override");
|
||||
expect(triggerAssignment?.heading).toBe("Kontext");
|
||||
expect(triggerAssignment?.noteType).toBe("experience");
|
||||
});
|
||||
|
||||
it("should use noteType as effectiveType when section has no [!section] callout", async () => {
|
||||
const app = createVaultAppFromFixtures();
|
||||
const chainRoles = loadChainRolesFromFixtures();
|
||||
const chainTemplates = loadChainTemplatesFromFixtures();
|
||||
|
||||
if (!chainRoles || !chainTemplates) {
|
||||
console.warn("Skipping test: real config files not found in fixtures");
|
||||
return;
|
||||
}
|
||||
|
||||
// 01_experience_trigger.md has type: experience, section "Kontext" has NO [!section] callout
|
||||
const file01 = app.vault.getAbstractFileByPath("Tests/01_experience_trigger.md");
|
||||
if (!file01 || !("extension" in file01) || file01.extension !== "md") {
|
||||
throw new Error("Fixture Tests/01_experience_trigger.md not found");
|
||||
}
|
||||
|
||||
const { edges: edges01 } = await buildNoteIndex(app, file01 as TFile);
|
||||
const allEdges: IndexedEdge[] = [...edges01];
|
||||
const file03 = app.vault.getAbstractFileByPath("Tests/03_insight_transformation.md");
|
||||
if (file03 && "extension" in file03 && file03.extension === "md") {
|
||||
const { edges: edges03 } = await buildNoteIndex(app, file03 as TFile);
|
||||
allEdges.push(...edges03);
|
||||
}
|
||||
const file04 = app.vault.getAbstractFileByPath("Tests/04_decision_outcome.md");
|
||||
if (file04 && "extension" in file04 && file04.extension === "md") {
|
||||
const { edges: edges04 } = await buildNoteIndex(app, file04 as TFile);
|
||||
allEdges.push(...edges04);
|
||||
}
|
||||
|
||||
const matches = await matchTemplates(
|
||||
app,
|
||||
{ file: "Tests/01_experience_trigger.md", heading: "Kontext" },
|
||||
allEdges,
|
||||
chainTemplates,
|
||||
chainRoles,
|
||||
mockEdgeVocabulary,
|
||||
{ includeNoteLinks: true, includeCandidates: false }
|
||||
);
|
||||
|
||||
const match = matches.find((m) => m.templateName === "trigger_transformation_outcome");
|
||||
expect(match).toBeDefined();
|
||||
const triggerAssignment = match?.slotAssignments?.trigger;
|
||||
expect(triggerAssignment).toBeDefined();
|
||||
// No [!section] → effectiveType = noteType from frontmatter = "experience"
|
||||
expect(triggerAssignment?.noteType).toBe("experience");
|
||||
});
|
||||
});
|
||||
|
|
@ -34,12 +34,28 @@ Content.
|
|||
>> [!edge] causes
|
||||
>> [[Link1]]
|
||||
> ^map-123
|
||||
`;
|
||||
const lines = sectionContent.split(/\n/);
|
||||
const state = extractExistingMappings(sectionContent, "abstract", "🕸️ Semantic Mapping");
|
||||
const abstractLineIdx = lines.findIndex((l) => l.includes("[!abstract]"));
|
||||
expect(state.wrapperBlockStartLine).toBe(abstractLineIdx);
|
||||
expect(state.wrapperBlockEndLine).toBeGreaterThan(state.wrapperBlockStartLine!);
|
||||
expect(state.wrapperBlockEndLine).toBeLessThanOrEqual(lines.length);
|
||||
});
|
||||
|
||||
it("should find abstract block via permissive fallback when title does not match exactly", () => {
|
||||
const sectionContent = `# Section
|
||||
|
||||
> [!abstract]- Semantic Mapping
|
||||
>> [!edge] causes
|
||||
>> [[Link1]]
|
||||
`;
|
||||
|
||||
const state = extractExistingMappings(sectionContent, "abstract", "🕸️ Semantic Mapping");
|
||||
|
||||
expect(state.wrapperBlockStartLine).toBe(3);
|
||||
expect(state.wrapperBlockEndLine).toBe(7); // After block-id marker
|
||||
expect(state.wrapperBlockStartLine).not.toBeNull();
|
||||
expect(state.wrapperBlockEndLine).not.toBeNull();
|
||||
expect(state.existingMappings.get("Link1")).toBe("causes");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,16 +21,16 @@ Even more content.
|
|||
`;
|
||||
|
||||
const sections = splitIntoSections(markdown);
|
||||
|
||||
expect(sections).toHaveLength(4);
|
||||
expect(sections[0]?.heading).toBeNull();
|
||||
expect(sections[0]?.headingLevel).toBe(0);
|
||||
expect(sections[1]?.heading).toBe("Heading 1");
|
||||
expect(sections[1]?.headingLevel).toBe(1);
|
||||
expect(sections[2]?.heading).toBe("Heading 2");
|
||||
expect(sections[2]?.headingLevel).toBe(2);
|
||||
expect(sections[3]?.heading).toBe("Heading 3");
|
||||
expect(sections[3]?.headingLevel).toBe(3);
|
||||
// When note starts with a heading, no "content before first heading" section: 3 sections
|
||||
expect(sections).toHaveLength(3);
|
||||
expect(sections[0]?.heading).toBe("Heading 1");
|
||||
expect(sections[0]?.headingLevel).toBe(1);
|
||||
expect(sections[1]?.heading).toBe("Heading 2");
|
||||
expect(sections[1]?.headingLevel).toBe(2);
|
||||
expect(sections[2]?.heading).toBe("Heading 3");
|
||||
expect(sections[2]?.headingLevel).toBe(3);
|
||||
expect(sections[0]?.sectionType).toBeNull();
|
||||
expect(sections[0]?.blockId).toBeNull();
|
||||
});
|
||||
|
||||
it("should extract links per section", () => {
|
||||
|
|
@ -44,11 +44,11 @@ More content with [[Link3]].
|
|||
`;
|
||||
|
||||
const sections = splitIntoSections(markdown);
|
||||
|
||||
expect(sections[1]?.links).toContain("Link1");
|
||||
expect(sections[1]?.links).toContain("Link2");
|
||||
expect(sections[2]?.links).toContain("Link3");
|
||||
expect(sections[2]?.links).not.toContain("Link1");
|
||||
// Sections: [Section 1, Section 2] when note starts with #
|
||||
expect(sections[0]?.links).toContain("Link1");
|
||||
expect(sections[0]?.links).toContain("Link2");
|
||||
expect(sections[1]?.links).toContain("Link3");
|
||||
expect(sections[1]?.links).not.toContain("Link1");
|
||||
});
|
||||
|
||||
it("should handle note without headings", () => {
|
||||
|
|
@ -60,6 +60,128 @@ More content with [[Link3]].
|
|||
expect(sections[0]?.heading).toBeNull();
|
||||
expect(sections[0]?.links).toContain("Link1");
|
||||
expect(sections[0]?.links).toContain("Link2");
|
||||
expect(sections[0]?.sectionType).toBeNull();
|
||||
expect(sections[0]?.blockId).toBeNull();
|
||||
});
|
||||
|
||||
it("should parse [!section] callout and set sectionType", () => {
|
||||
const markdown = `## Kontext
|
||||
|
||||
> [!section] experience
|
||||
|
||||
Content here.
|
||||
`;
|
||||
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.heading).toBe("Kontext");
|
||||
expect(sections[0]?.sectionType).toBe("experience");
|
||||
expect(sections[0]?.blockId).toBeNull();
|
||||
});
|
||||
|
||||
it("should parse block ID from heading (^block-id)", () => {
|
||||
const markdown = `## Situation ^sit
|
||||
|
||||
Content.
|
||||
`;
|
||||
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.heading).toBe("Situation ^sit");
|
||||
expect(sections[0]?.blockId).toBe("sit");
|
||||
expect(sections[0]?.sectionType).toBeNull();
|
||||
});
|
||||
|
||||
it("should parse both sectionType and blockId", () => {
|
||||
const markdown = `## Reflexion ^ref
|
||||
|
||||
> [!section] insight
|
||||
|
||||
Text.
|
||||
`;
|
||||
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.heading).toBe("Reflexion ^ref");
|
||||
expect(sections[0]?.blockId).toBe("ref");
|
||||
expect(sections[0]?.sectionType).toBe("insight");
|
||||
});
|
||||
|
||||
it("should parse [!section] with different casing (case-insensitive)", () => {
|
||||
const markdown = `## Test
|
||||
|
||||
> [!SECTION] decision
|
||||
`;
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.sectionType).toBe("decision");
|
||||
});
|
||||
|
||||
it("should parse block ID with underscores and hyphens", () => {
|
||||
const markdown = `## My Section ^my_block-id_01
|
||||
|
||||
Content.
|
||||
`;
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.blockId).toBe("my_block-id_01");
|
||||
});
|
||||
|
||||
it("should not set sectionType when no [!section] callout in section", () => {
|
||||
const markdown = `## Only Heading
|
||||
|
||||
> [!abstract] Some other callout
|
||||
Content.
|
||||
`;
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.sectionType).toBeNull();
|
||||
});
|
||||
|
||||
it("should use last [!section] when multiple section callouts in same section (last wins)", () => {
|
||||
const markdown = `## Multi
|
||||
|
||||
> [!section] experience
|
||||
> [!section] insight
|
||||
Content.
|
||||
`;
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.sectionType).toBe("insight");
|
||||
});
|
||||
|
||||
it("should set sectionType only for section that contains the callout", () => {
|
||||
const markdown = `## First
|
||||
|
||||
No callout here.
|
||||
|
||||
## Second
|
||||
|
||||
> [!section] insight
|
||||
|
||||
Text.
|
||||
`;
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(2);
|
||||
expect(sections[0]?.heading).toBe("First");
|
||||
expect(sections[0]?.sectionType).toBeNull();
|
||||
expect(sections[1]?.heading).toBe("Second");
|
||||
expect(sections[1]?.sectionType).toBe("insight");
|
||||
});
|
||||
|
||||
it("should preserve startLine and endLine with sectionType and blockId", () => {
|
||||
const markdown = `## A ^a
|
||||
|
||||
> [!section] experience
|
||||
|
||||
X
|
||||
`;
|
||||
const sections = splitIntoSections(markdown);
|
||||
expect(sections).toHaveLength(1);
|
||||
expect(sections[0]?.startLine).toBe(0);
|
||||
expect(sections[0]?.endLine).toBeGreaterThan(0);
|
||||
expect(sections[0]?.sectionType).toBe("experience");
|
||||
expect(sections[0]?.blockId).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { normalizeLinkTarget, isUnresolvedLink, waitForFileModify, parseWikilinkAtPosition } from "../../unresolvedLink/linkHelpers";
|
||||
import { normalizeLinkTarget, normalizeHeadingForMatch, headingsMatch, isUnresolvedLink, waitForFileModify, parseWikilinkAtPosition } from "../../unresolvedLink/linkHelpers";
|
||||
import { App, TFile } from "obsidian";
|
||||
|
||||
describe("normalizeLinkTarget", () => {
|
||||
|
|
@ -37,6 +37,51 @@ describe("normalizeLinkTarget", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("normalizeHeadingForMatch", () => {
|
||||
it("returns null for null/undefined/empty", () => {
|
||||
expect(normalizeHeadingForMatch(null)).toBe(null);
|
||||
expect(normalizeHeadingForMatch(undefined as unknown as null)).toBe(null);
|
||||
expect(normalizeHeadingForMatch("")).toBe(null);
|
||||
expect(normalizeHeadingForMatch(" ")).toBe(null);
|
||||
});
|
||||
|
||||
it("strips block-id with caret (Obsidian source form)", () => {
|
||||
expect(normalizeHeadingForMatch("Überschrift ^Block")).toBe("Überschrift");
|
||||
expect(normalizeHeadingForMatch("Kontext ^context-block")).toBe("Kontext");
|
||||
});
|
||||
|
||||
it("strips one trailing word (Obsidian UI link form)", () => {
|
||||
expect(normalizeHeadingForMatch("Überschrift Block")).toBe("Überschrift");
|
||||
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
|
||||
});
|
||||
|
||||
it("leaves heading without block suffix unchanged", () => {
|
||||
expect(normalizeHeadingForMatch("Überschrift")).toBe("Überschrift");
|
||||
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
|
||||
});
|
||||
});
|
||||
|
||||
describe("headingsMatch", () => {
|
||||
it("matches same heading", () => {
|
||||
expect(headingsMatch("Überschrift", "Überschrift")).toBe(true);
|
||||
expect(headingsMatch(null, null)).toBe(true);
|
||||
});
|
||||
|
||||
it("matches heading with block-id variants", () => {
|
||||
expect(headingsMatch("Überschrift", "Überschrift ^Block")).toBe(true);
|
||||
expect(headingsMatch("Überschrift ^Block", "Überschrift")).toBe(true);
|
||||
expect(headingsMatch("Überschrift Block", "Überschrift")).toBe(true);
|
||||
expect(headingsMatch("Überschrift", "Überschrift Block")).toBe(true);
|
||||
expect(headingsMatch("Überschrift ^Block", "Überschrift Block")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match different headings", () => {
|
||||
expect(headingsMatch("Überschrift", "Andere")).toBe(false);
|
||||
expect(headingsMatch("Überschrift", null)).toBe(false);
|
||||
expect(headingsMatch(null, "Überschrift")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseWikilinkAtPosition", () => {
|
||||
it("finds wikilink at cursor position", () => {
|
||||
const line = "This is a [[test-note]] link";
|
||||
|
|
|
|||
159
src/tests/workbench/insertEdgeIntoSectionContent.test.ts
Normal file
159
src/tests/workbench/insertEdgeIntoSectionContent.test.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* Tests for insertEdgeIntoSectionContent: edge insertion into abstract block.
|
||||
* Ensures: (1) new edges go into abstract block with ">>" blank line; (2) same edge type appends to existing group.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { computeSectionContentAfterInsertEdge } from "../../workbench/insertEdgeIntoSectionContent";
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
wrapperCalloutType: "abstract",
|
||||
wrapperTitle: "🕸️ Semantic Mapping",
|
||||
wrapperFolded: true,
|
||||
};
|
||||
|
||||
describe("computeSectionContentAfterInsertEdge", () => {
|
||||
it("should insert new edge type into existing abstract block, separated by single line with >", () => {
|
||||
const sectionContent = `## Reflexion & Learning
|
||||
|
||||
Text before.
|
||||
|
||||
> [!abstract]- 🕸️ Semantic Mapping
|
||||
>> [!edge] foundation_for
|
||||
>> [[#^next]]
|
||||
|
||||
More text.
|
||||
`;
|
||||
|
||||
const result = computeSectionContentAfterInsertEdge(
|
||||
sectionContent,
|
||||
"guides",
|
||||
"Geburt unserer Kinder Rouven und Rohan#Nächster Schritt ^next",
|
||||
DEFAULT_OPTIONS
|
||||
);
|
||||
|
||||
// Must still contain the abstract block and both edge groups
|
||||
expect(result).toContain("> [!abstract]- 🕸️ Semantic Mapping");
|
||||
expect(result).toContain(">> [!edge] foundation_for");
|
||||
expect(result).toContain(">> [!edge] guides");
|
||||
expect(result).toContain(">> [[Geburt unserer Kinder Rouven und Rohan#Nächster Schritt ^next]]");
|
||||
|
||||
// Separator between edge groups must be exactly one line with exactly one ">"
|
||||
const guidesIdx = result.indexOf(">> [!edge] guides");
|
||||
const foundationIdx = result.indexOf(">> [!edge] foundation_for");
|
||||
expect(guidesIdx).toBeGreaterThan(foundationIdx);
|
||||
const between = result.slice(foundationIdx + ">> [!edge] foundation_for".length, guidesIdx);
|
||||
expect(between).toMatch(/\n>\n/);
|
||||
expect(between).not.toMatch(/\n\n\n/);
|
||||
});
|
||||
|
||||
it("should append to existing edge type group when same type already exists", () => {
|
||||
const sectionContent = `## Section
|
||||
|
||||
> [!abstract]- 🕸️ Semantic Mapping
|
||||
>> [!edge] guides
|
||||
>> [[TargetA#^a]]
|
||||
`;
|
||||
|
||||
const result = computeSectionContentAfterInsertEdge(
|
||||
sectionContent,
|
||||
"guides",
|
||||
"TargetB#^b",
|
||||
DEFAULT_OPTIONS
|
||||
);
|
||||
|
||||
expect(result).toContain(">> [!edge] guides");
|
||||
expect(result).toContain(">> [[TargetA#^a]]");
|
||||
expect(result).toContain(">> [[TargetB#^b]]");
|
||||
// Only one edge header for guides
|
||||
const guidesCount = (result.match(/>>\s*\[!edge\]\s+guides/g) || []).length;
|
||||
expect(guidesCount).toBe(1);
|
||||
// Both targets under that header
|
||||
const afterGuides = result.split(">> [!edge] guides")[1] ?? "";
|
||||
expect(afterGuides).toContain("[[TargetA#^a]]");
|
||||
expect(afterGuides).toContain("[[TargetB#^b]]");
|
||||
});
|
||||
|
||||
it("should create new mapping block at end when section has no abstract block", () => {
|
||||
const sectionContent = `## Section
|
||||
|
||||
Some text.
|
||||
`;
|
||||
|
||||
const result = computeSectionContentAfterInsertEdge(
|
||||
sectionContent,
|
||||
"guides",
|
||||
"OtherNote#Section",
|
||||
DEFAULT_OPTIONS
|
||||
);
|
||||
|
||||
expect(result).toContain("Some text.");
|
||||
expect(result).toContain("> [!abstract]- 🕸️ Semantic Mapping");
|
||||
expect(result).toContain(">> [!edge] guides");
|
||||
expect(result).toContain(">> [[OtherNote#Section]]");
|
||||
});
|
||||
|
||||
it("should find abstract block with permissive detection when title differs", () => {
|
||||
const sectionContent = `## Reflexion
|
||||
|
||||
> [!abstract]- Semantic Mapping
|
||||
>> [!edge] related_to
|
||||
>> [[#^impact]]
|
||||
`;
|
||||
|
||||
const result = computeSectionContentAfterInsertEdge(
|
||||
sectionContent,
|
||||
"guides",
|
||||
"Note#^next",
|
||||
{ ...DEFAULT_OPTIONS, wrapperTitle: "🕸️ Semantic Mapping" }
|
||||
);
|
||||
|
||||
// Must insert into existing block (permissive finds "Semantic Mapping" block)
|
||||
expect(result).toContain(">> [!edge] related_to");
|
||||
expect(result).toContain(">> [!edge] guides");
|
||||
expect(result).toContain(">> [[Note#^next]]");
|
||||
const abstractCount = (result.match(/\[!abstract\]/g) || []).length;
|
||||
expect(abstractCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should produce exact format: one line with single > between edge blocks, no double blank lines", () => {
|
||||
const sectionContent = `## Reflexion
|
||||
|
||||
> [!abstract]- 🕸️ Semantic Mapping
|
||||
>> [!edge] related_to
|
||||
>> [[#^impact]]
|
||||
`;
|
||||
|
||||
const result = computeSectionContentAfterInsertEdge(
|
||||
sectionContent,
|
||||
"foundation_for",
|
||||
"#^next",
|
||||
DEFAULT_OPTIONS
|
||||
);
|
||||
|
||||
// Must have exactly one ">" line between related_to block and foundation_for block
|
||||
expect(result).toContain(">> [!edge] related_to");
|
||||
expect(result).toContain(">> [[#^impact]]");
|
||||
expect(result).toContain(">> [!edge] foundation_for");
|
||||
expect(result).toContain(">> [[#^next]]");
|
||||
const between = result.split(">> [[#^impact]]")[1]?.split(">> [!edge] foundation_for")[0] ?? "";
|
||||
expect(between.trim()).toBe(">");
|
||||
expect(result).not.toMatch(/\n\n\n/);
|
||||
});
|
||||
|
||||
it("should separate new edge group with single > line even when block has no trailing newline", () => {
|
||||
const sectionContent = `## S
|
||||
|
||||
> [!abstract]- 🕸️ Semantic Mapping
|
||||
>> [!edge] foundation_for
|
||||
>> [[#^x]]`;
|
||||
|
||||
const result = computeSectionContentAfterInsertEdge(sectionContent, "guides", "Y#^y", DEFAULT_OPTIONS);
|
||||
|
||||
expect(result).toContain(">> [!edge] foundation_for");
|
||||
expect(result).toContain(">> [!edge] guides");
|
||||
expect(result).toContain(">> [[#^x]]");
|
||||
expect(result).toContain(">> [[Y#^y]]");
|
||||
expect(result).toMatch(/\n>\n>> \[!edge\] guides/);
|
||||
});
|
||||
});
|
||||
|
|
@ -154,9 +154,9 @@ describe("generateTodos", () => {
|
|||
confidence: "confirmed",
|
||||
roleEvidence: [
|
||||
{
|
||||
// roleEvidence uses nodeKey format: "file:heading"
|
||||
from: "file1.md:",
|
||||
to: "file2.md:",
|
||||
// roleEvidence uses slot IDs (from/to match template link.from/link.to)
|
||||
from: "slot1",
|
||||
to: "slot2",
|
||||
edgeRole: "causal",
|
||||
rawEdgeType: "caused_by",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Chain Workbench Modal - UI for chain workbench.
|
||||
*/
|
||||
|
||||
import { Modal, Setting, Notice } from "obsidian";
|
||||
import { Modal, Setting, Notice, TFile } from "obsidian";
|
||||
import type { App } from "obsidian";
|
||||
import type { WorkbenchModel, WorkbenchMatch, WorkbenchTodoUnion } from "../workbench/types";
|
||||
import type { MindnetSettings } from "../settings";
|
||||
|
|
@ -37,6 +37,9 @@ export class ChainWorkbenchModal extends Modal {
|
|||
this.chainTemplates = chainTemplates;
|
||||
this.vocabulary = vocabulary;
|
||||
this.pluginInstance = pluginInstance;
|
||||
|
||||
// Add CSS class for wide modal
|
||||
this.modalEl.addClass("chain-workbench-modal");
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
|
|
@ -44,8 +47,10 @@ export class ChainWorkbenchModal extends Modal {
|
|||
contentEl.empty();
|
||||
|
||||
// Header
|
||||
contentEl.createEl("h2", { text: "Chain Workbench" });
|
||||
contentEl.createEl("p", {
|
||||
const header = contentEl.createDiv({ cls: "workbench-header" });
|
||||
header.createEl("h2", { text: "Chain Workbench" });
|
||||
header.createEl("p", {
|
||||
cls: "context-info",
|
||||
text: `Context: ${this.model.context.file}${this.model.context.heading ? `#${this.model.context.heading}` : ""}`,
|
||||
});
|
||||
|
||||
|
|
@ -72,25 +77,16 @@ export class ChainWorkbenchModal extends Modal {
|
|||
this.render();
|
||||
});
|
||||
|
||||
// Main container: list + details
|
||||
// Main container: two-column layout
|
||||
const mainContainer = contentEl.createDiv({ cls: "workbench-main" });
|
||||
|
||||
// Left: Match list
|
||||
const listContainer = mainContainer.createDiv({ cls: "workbench-list" });
|
||||
listContainer.createEl("h3", { text: "Template Matches" });
|
||||
// Left: Tree View
|
||||
const treeContainer = mainContainer.createDiv({ cls: "workbench-tree" });
|
||||
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
||||
|
||||
// Right: Details
|
||||
// Right: Details View
|
||||
const detailsContainer = mainContainer.createDiv({ cls: "workbench-details" });
|
||||
detailsContainer.createEl("h3", { text: "Details" });
|
||||
|
||||
// Global todos section (if any)
|
||||
if (this.model.globalTodos && this.model.globalTodos.length > 0) {
|
||||
const globalTodosSection = contentEl.createDiv({ cls: "workbench-global-todos" });
|
||||
globalTodosSection.createEl("h3", { text: "Section-Level Issues" });
|
||||
globalTodosSection.createEl("p", {
|
||||
text: `Found ${this.model.globalTodos.length} issue(s) from Chain Inspector findings`,
|
||||
});
|
||||
}
|
||||
detailsContainer.createEl("h3", { text: "Chain Details" });
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -116,46 +112,92 @@ export class ChainWorkbenchModal extends Modal {
|
|||
return b.score - a.score;
|
||||
});
|
||||
|
||||
// Render list
|
||||
const listContainer = contentEl.querySelector(".workbench-list");
|
||||
if (listContainer) {
|
||||
listContainer.empty();
|
||||
listContainer.createEl("h3", { text: `Template Matches (${filteredMatches.length})` });
|
||||
// Render tree view (left column)
|
||||
this.renderTreeView(filteredMatches);
|
||||
|
||||
for (const match of filteredMatches) {
|
||||
const matchEl = listContainer.createDiv({ cls: "workbench-match-item" });
|
||||
// Render details (right column)
|
||||
this.renderDetails();
|
||||
}
|
||||
|
||||
private renderTreeView(matches: WorkbenchMatch[]): void {
|
||||
const treeContainer = this.contentEl.querySelector(".workbench-tree");
|
||||
if (!treeContainer) return;
|
||||
|
||||
treeContainer.empty();
|
||||
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
||||
|
||||
// Group matches by template name
|
||||
const matchesByTemplate = new Map<string, WorkbenchMatch[]>();
|
||||
for (const match of matches) {
|
||||
if (!matchesByTemplate.has(match.templateName)) {
|
||||
matchesByTemplate.set(match.templateName, []);
|
||||
}
|
||||
matchesByTemplate.get(match.templateName)!.push(match);
|
||||
}
|
||||
|
||||
// Create template tree
|
||||
const templateTree = treeContainer.createDiv({ cls: "template-tree" });
|
||||
|
||||
for (const [templateName, templateMatches] of matchesByTemplate.entries()) {
|
||||
const templateItem = templateTree.createDiv({ cls: "template-tree-item" });
|
||||
|
||||
const templateHeader = templateItem.createDiv({ cls: "template-tree-header" });
|
||||
templateHeader.createSpan({ cls: "template-tree-toggle", text: "▶" });
|
||||
templateHeader.createSpan({ cls: "template-tree-name", text: templateName });
|
||||
templateHeader.createSpan({ cls: "template-tree-count", text: `(${templateMatches.length})` });
|
||||
|
||||
const chainsContainer = templateItem.createDiv({ cls: "template-tree-chains" });
|
||||
|
||||
// Add chains for this template
|
||||
for (const match of templateMatches) {
|
||||
const chainItem = chainsContainer.createDiv({ cls: "chain-item" });
|
||||
if (this.selectedMatch === match) {
|
||||
matchEl.addClass("selected");
|
||||
chainItem.addClass("selected");
|
||||
}
|
||||
|
||||
const statusIcon = this.getStatusIcon(match.status);
|
||||
matchEl.createEl("div", {
|
||||
cls: "match-header",
|
||||
text: `${statusIcon} ${match.templateName}`,
|
||||
const chainHeader = chainItem.createDiv({ cls: "chain-item-header" });
|
||||
chainHeader.createSpan({
|
||||
cls: "chain-status-icon",
|
||||
text: this.getStatusIcon(match.status)
|
||||
});
|
||||
chainHeader.createSpan({
|
||||
text: `Chain #${templateMatches.indexOf(match) + 1}`
|
||||
});
|
||||
|
||||
matchEl.createEl("div", {
|
||||
cls: "match-stats",
|
||||
text: `Slots: ${match.slotsFilled}/${match.slotsTotal} | Links: ${match.satisfiedLinks}/${match.requiredLinks} | Score: ${match.score}`,
|
||||
chainItem.createDiv({
|
||||
cls: "chain-item-info",
|
||||
text: `Slots: ${match.slotsFilled}/${match.slotsTotal} | Links: ${match.satisfiedLinks}/${match.requiredLinks} | Score: ${match.score}`
|
||||
});
|
||||
|
||||
matchEl.createEl("div", {
|
||||
cls: "match-todos-count",
|
||||
text: `${match.todos.length} todo(s)`,
|
||||
});
|
||||
// Show affected notes
|
||||
const notes = new Set<string>();
|
||||
for (const assignment of Object.values(match.slotAssignments)) {
|
||||
if (assignment) {
|
||||
notes.add(assignment.file);
|
||||
}
|
||||
}
|
||||
if (notes.size > 0) {
|
||||
chainItem.createDiv({
|
||||
cls: "chain-item-notes",
|
||||
text: `Notes: ${Array.from(notes).slice(0, 3).join(", ")}${notes.size > 3 ? "..." : ""}`
|
||||
});
|
||||
}
|
||||
|
||||
matchEl.addEventListener("click", () => {
|
||||
chainItem.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.selectedMatch = match;
|
||||
this.renderDetails();
|
||||
this.render();
|
||||
});
|
||||
|
||||
templateHeader.addEventListener("click", () => {
|
||||
templateItem.classList.toggle("expanded");
|
||||
const toggle = templateHeader.querySelector(".template-tree-toggle");
|
||||
if (toggle) {
|
||||
toggle.textContent = templateItem.classList.contains("expanded") ? "▼" : "▶";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Render details
|
||||
this.renderDetails();
|
||||
|
||||
// Render global todos
|
||||
this.renderGlobalTodos();
|
||||
}
|
||||
|
||||
private renderGlobalTodos(): void {
|
||||
|
|
@ -204,55 +246,279 @@ export class ChainWorkbenchModal extends Modal {
|
|||
}
|
||||
|
||||
private renderDetails(): void {
|
||||
const detailsContainer = this.contentEl.querySelector(".workbench-details");
|
||||
const detailsContainer = this.contentEl.querySelector(".workbench-details") as HTMLElement;
|
||||
if (!detailsContainer) return;
|
||||
|
||||
detailsContainer.empty();
|
||||
detailsContainer.createEl("h3", { text: "Details" });
|
||||
detailsContainer.createEl("h3", { text: "Chain Details" });
|
||||
|
||||
if (!this.selectedMatch) {
|
||||
detailsContainer.createEl("p", { text: "Select a match to view details" });
|
||||
detailsContainer.createDiv({
|
||||
cls: "workbench-details-placeholder",
|
||||
text: "Wählen Sie eine Chain aus der linken Liste aus, um Details anzuzeigen"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const match = this.selectedMatch;
|
||||
const template = this.chainTemplates?.templates?.find(t => t.name === match.templateName);
|
||||
|
||||
// Match info
|
||||
detailsContainer.createEl("h4", { text: match.templateName });
|
||||
detailsContainer.createEl("p", {
|
||||
text: `Status: ${match.status} | Score: ${match.score} | Confidence: ${match.confidence}`,
|
||||
// Chain Header
|
||||
const header = detailsContainer.createDiv({ cls: "chain-visualization-header" });
|
||||
header.createEl("h4", { text: match.templateName });
|
||||
header.createDiv({
|
||||
cls: "chain-meta",
|
||||
text: `Status: ${match.status} | Score: ${match.score} | Confidence: ${match.confidence} | Slots: ${match.slotsFilled}/${match.slotsTotal} | Links: ${match.satisfiedLinks}/${match.requiredLinks}`
|
||||
});
|
||||
|
||||
// Slots
|
||||
detailsContainer.createEl("h5", { text: "Slots" });
|
||||
const slotsList = detailsContainer.createEl("ul");
|
||||
for (const [slotId, assignment] of Object.entries(match.slotAssignments)) {
|
||||
if (assignment) {
|
||||
const nodeStr = assignment.heading
|
||||
? `${assignment.file}#${assignment.heading}`
|
||||
: assignment.file;
|
||||
slotsList.createEl("li", { text: `${slotId}: ${nodeStr} [${assignment.noteType}]` });
|
||||
// Chain Visualization
|
||||
const visualization = detailsContainer.createDiv({ cls: "chain-visualization" });
|
||||
visualization.createEl("h4", { text: "Chain Flow" });
|
||||
|
||||
// Render chain flow as table
|
||||
this.renderChainFlowTable(visualization, match, template);
|
||||
|
||||
// Edge Detection Details
|
||||
this.renderEdgeDetectionDetails(detailsContainer, match, template);
|
||||
|
||||
// Todos Section
|
||||
this.renderTodosSection(detailsContainer, match);
|
||||
}
|
||||
|
||||
private renderChainFlowTable(
|
||||
container: HTMLElement,
|
||||
match: WorkbenchMatch,
|
||||
template: import("../dictionary/types").ChainTemplate | undefined
|
||||
): void {
|
||||
const table = container.createEl("table", { cls: "chain-flow-table" });
|
||||
|
||||
// Header
|
||||
const thead = table.createEl("thead");
|
||||
const headerRow = thead.createEl("tr");
|
||||
headerRow.createEl("th", { text: "Von Slot" });
|
||||
headerRow.createEl("th", { text: "Referenz" });
|
||||
headerRow.createEl("th", { text: "→" });
|
||||
headerRow.createEl("th", { text: "Edge Type" });
|
||||
headerRow.createEl("th", { text: "Rolle" });
|
||||
headerRow.createEl("th", { text: "→" });
|
||||
headerRow.createEl("th", { text: "Zu Slot" });
|
||||
headerRow.createEl("th", { text: "Referenz" });
|
||||
|
||||
const tbody = table.createEl("tbody");
|
||||
|
||||
if (!template || !template.links || template.links.length === 0) {
|
||||
const row = tbody.createEl("tr");
|
||||
row.createEl("td", {
|
||||
attr: { colspan: "7" },
|
||||
text: "Keine Links im Template definiert",
|
||||
cls: "missing"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Build chain flow: follow links in order
|
||||
const links = template.links;
|
||||
const visitedSlots = new Set<string>();
|
||||
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
const link = links[i];
|
||||
if (!link) continue;
|
||||
const fromSlot = link.from;
|
||||
const toSlot = link.to;
|
||||
|
||||
const row = tbody.createEl("tr");
|
||||
|
||||
// From Slot
|
||||
const fromAssignment = match.slotAssignments[fromSlot];
|
||||
if (fromAssignment) {
|
||||
row.createEl("td", {
|
||||
cls: "slot-cell",
|
||||
text: fromSlot
|
||||
});
|
||||
const refCell = row.createEl("td", { cls: "node-cell" });
|
||||
const refText = fromAssignment.heading
|
||||
? `${fromAssignment.file}#${fromAssignment.heading}`
|
||||
: fromAssignment.file;
|
||||
refCell.createEl("code", { text: refText });
|
||||
refCell.createEl("br");
|
||||
refCell.createSpan({
|
||||
text: `[${fromAssignment.noteType}]`,
|
||||
cls: "node-type-badge"
|
||||
});
|
||||
} else {
|
||||
row.createEl("td", {
|
||||
cls: "slot-cell missing",
|
||||
text: fromSlot
|
||||
});
|
||||
row.createEl("td", {
|
||||
cls: "missing",
|
||||
text: "FEHLEND"
|
||||
});
|
||||
}
|
||||
|
||||
// Arrow
|
||||
row.createEl("td", {
|
||||
cls: "edge-cell",
|
||||
text: "→"
|
||||
});
|
||||
|
||||
// Edge Type
|
||||
const edgeEvidence = match.roleEvidence?.find(e => e.from === fromSlot && e.to === toSlot);
|
||||
if (edgeEvidence) {
|
||||
row.createEl("td", {
|
||||
cls: "edge-cell",
|
||||
text: edgeEvidence.rawEdgeType
|
||||
});
|
||||
row.createEl("td", {
|
||||
cls: "edge-cell",
|
||||
text: `[${edgeEvidence.edgeRole}]`
|
||||
});
|
||||
} else {
|
||||
row.createEl("td", {
|
||||
cls: "edge-cell missing",
|
||||
text: "FEHLEND"
|
||||
});
|
||||
row.createEl("td", {
|
||||
cls: "edge-cell missing",
|
||||
text: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Arrow
|
||||
row.createEl("td", {
|
||||
cls: "edge-cell",
|
||||
text: "→"
|
||||
});
|
||||
|
||||
// To Slot
|
||||
const toAssignment = match.slotAssignments[toSlot];
|
||||
if (toAssignment) {
|
||||
row.createEl("td", {
|
||||
cls: "slot-cell",
|
||||
text: toSlot
|
||||
});
|
||||
const refCell = row.createEl("td", { cls: "node-cell" });
|
||||
const refText = toAssignment.heading
|
||||
? `${toAssignment.file}#${toAssignment.heading}`
|
||||
: toAssignment.file;
|
||||
refCell.createEl("code", { text: refText });
|
||||
refCell.createEl("br");
|
||||
refCell.createSpan({
|
||||
text: `[${toAssignment.noteType}]`,
|
||||
cls: "node-type-badge"
|
||||
});
|
||||
} else {
|
||||
row.createEl("td", {
|
||||
cls: "slot-cell missing",
|
||||
text: toSlot
|
||||
});
|
||||
row.createEl("td", {
|
||||
cls: "missing",
|
||||
text: "FEHLEND"
|
||||
});
|
||||
}
|
||||
}
|
||||
if (match.missingSlots.length > 0) {
|
||||
slotsList.createEl("li", {
|
||||
text: `Missing: ${match.missingSlots.join(", ")}`,
|
||||
cls: "missing",
|
||||
}
|
||||
|
||||
private renderEdgeDetectionDetails(
|
||||
container: HTMLElement,
|
||||
match: WorkbenchMatch,
|
||||
template: import("../dictionary/types").ChainTemplate | undefined
|
||||
): void {
|
||||
const section = container.createDiv({ cls: "edge-detection-details" });
|
||||
section.createEl("h4", { text: "Edge-Erkennung" });
|
||||
|
||||
if (!template || !template.links || template.links.length === 0) {
|
||||
section.createEl("p", {
|
||||
text: "Keine Links im Template definiert",
|
||||
cls: "missing"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Links
|
||||
detailsContainer.createEl("h5", { text: "Links" });
|
||||
detailsContainer.createEl("p", {
|
||||
text: `${match.satisfiedLinks}/${match.requiredLinks} satisfied (${match.linksComplete ? "complete" : "incomplete"})`,
|
||||
});
|
||||
const edgeList = section.createDiv({ cls: "edge-detection-list" });
|
||||
|
||||
// Todos
|
||||
detailsContainer.createEl("h5", { text: `Todos (${match.todos.length})` });
|
||||
const todosList = detailsContainer.createEl("div", { cls: "todos-list" });
|
||||
for (const link of template.links) {
|
||||
const edgeEvidence = match.roleEvidence?.find(e => e.from === link.from && e.to === link.to);
|
||||
const fromAssignment = match.slotAssignments[link.from];
|
||||
const toAssignment = match.slotAssignments[link.to];
|
||||
|
||||
const item = edgeList.createDiv({
|
||||
cls: `edge-detection-item ${edgeEvidence ? "matched" : "not-matched"}`
|
||||
});
|
||||
|
||||
const header = item.createDiv({ cls: "edge-detection-item-header" });
|
||||
header.createSpan({
|
||||
cls: `edge-detection-status ${edgeEvidence ? "matched" : "not-matched"}`,
|
||||
text: edgeEvidence ? "✓ GEFUNDEN" : "✗ NICHT GEFUNDEN"
|
||||
});
|
||||
header.createSpan({
|
||||
text: `${link.from} → ${link.to}`
|
||||
});
|
||||
|
||||
const info = item.createDiv({ cls: "edge-detection-info" });
|
||||
|
||||
if (edgeEvidence) {
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Edge Type:</span>
|
||||
<span><code>${edgeEvidence.rawEdgeType}</code> [${edgeEvidence.edgeRole}]</span>
|
||||
`;
|
||||
} else {
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Grund:</span>
|
||||
<span>Kein Edge zwischen ${link.from} und ${link.to} gefunden</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (fromAssignment && toAssignment) {
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Von:</span>
|
||||
<span><code>${fromAssignment.file}${fromAssignment.heading ? `#${fromAssignment.heading}` : ""}</code></span>
|
||||
`;
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Zu:</span>
|
||||
<span><code>${toAssignment.file}${toAssignment.heading ? `#${toAssignment.heading}` : ""}</code></span>
|
||||
`;
|
||||
} else {
|
||||
if (!fromAssignment) {
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Von Slot:</span>
|
||||
<span class="missing">${link.from} fehlt</span>
|
||||
`;
|
||||
}
|
||||
if (!toAssignment) {
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Zu Slot:</span>
|
||||
<span class="missing">${link.to} fehlt</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.allowed_edge_roles && link.allowed_edge_roles.length > 0) {
|
||||
info.createDiv({ cls: "info-row" }).innerHTML = `
|
||||
<span class="info-label">Erlaubte Rollen:</span>
|
||||
<span>${link.allowed_edge_roles.join(", ")}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderTodosSection(container: HTMLElement, match: WorkbenchMatch): void {
|
||||
const section = container.createDiv({ cls: "workbench-todos-section" });
|
||||
section.createEl("h4", { text: `Todos (${match.todos.length})` });
|
||||
|
||||
if (match.todos.length === 0) {
|
||||
section.createEl("p", {
|
||||
text: "Keine Todos für diese Chain",
|
||||
cls: "info-text"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const todosList = section.createDiv({ cls: "todos-list" });
|
||||
|
||||
for (const todo of match.todos) {
|
||||
const todoEl = todosList.createDiv({ cls: `todo-item todo-${todo.type}` });
|
||||
const todoEl = todosList.createDiv({ cls: `todo-item todo-${todo.type} ${todo.priority}` });
|
||||
todoEl.createEl("div", { cls: "todo-description", text: todo.description });
|
||||
todoEl.createEl("div", { cls: "todo-priority", text: `Priority: ${todo.priority}` });
|
||||
|
||||
|
|
@ -337,13 +603,68 @@ export class ChainWorkbenchModal extends Modal {
|
|||
case "insert_edge_forward":
|
||||
if (todo.type === "missing_link") {
|
||||
console.log("[Chain Workbench] Starting insert_edge_forward for missing_link todo");
|
||||
// Let user choose zone
|
||||
console.log("[Chain Workbench] Opening zone chooser...");
|
||||
const zoneChoice = await this.chooseZone(activeFile);
|
||||
console.log("[Chain Workbench] Zone choice:", zoneChoice);
|
||||
if (zoneChoice === null) {
|
||||
console.log("[Chain Workbench] User cancelled zone selection");
|
||||
return; // User cancelled
|
||||
|
||||
// Check if source note has defined sections (with > [!section] callouts)
|
||||
// If not, offer choice between section and note_links
|
||||
let zoneChoice: "section" | "note_links" | "candidates" = "section";
|
||||
|
||||
// Try to find source file
|
||||
let sourceFile: TFile | null = null;
|
||||
const fileRef = todo.fromNodeRef.file;
|
||||
const possiblePaths = [
|
||||
fileRef,
|
||||
fileRef + ".md",
|
||||
fileRef.replace(/\.md$/, ""),
|
||||
fileRef.replace(/\.md$/, "") + ".md",
|
||||
];
|
||||
const currentDir = activeFile.path.split("/").slice(0, -1).join("/");
|
||||
if (currentDir) {
|
||||
possiblePaths.push(
|
||||
`${currentDir}/${fileRef}`,
|
||||
`${currentDir}/${fileRef}.md`,
|
||||
`${currentDir}/${fileRef.replace(/\.md$/, "")}.md`
|
||||
);
|
||||
}
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
const found = this.app.vault.getAbstractFileByPath(path);
|
||||
if (found && found instanceof TFile) {
|
||||
sourceFile = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceFile) {
|
||||
const basename = fileRef.replace(/\.md$/, "").split("/").pop() || fileRef;
|
||||
const resolved = this.app.metadataCache.getFirstLinkpathDest(basename, activeFile.path);
|
||||
if (resolved) {
|
||||
sourceFile = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if source file has defined sections
|
||||
if (sourceFile) {
|
||||
const { splitIntoSections } = await import("../mapping/sectionParser");
|
||||
const content = await this.app.vault.read(sourceFile);
|
||||
const sections = splitIntoSections(content);
|
||||
|
||||
// Check if any section has sectionType defined (has > [!section] callout)
|
||||
const hasDefinedSections = sections.some(s => s.sectionType !== null);
|
||||
|
||||
// If no defined sections but multiple headings exist, offer choice
|
||||
if (!hasDefinedSections && sections.length > 1) {
|
||||
const contentSections = sections.filter(
|
||||
s => s.heading !== "Kandidaten" && s.heading !== "Note-Verbindungen"
|
||||
);
|
||||
if (contentSections.length > 0) {
|
||||
// Offer choice between section and note_links
|
||||
const choice = await this.chooseZone(sourceFile);
|
||||
if (choice === null) {
|
||||
return; // User cancelled
|
||||
}
|
||||
zoneChoice = choice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get edge vocabulary
|
||||
|
|
@ -354,7 +675,7 @@ export class ChainWorkbenchModal extends Modal {
|
|||
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
||||
console.log("[Chain Workbench] Edge vocabulary loaded, entries:", edgeVocabulary.byCanonical.size);
|
||||
|
||||
console.log("[Chain Workbench] Calling insertEdgeForward...");
|
||||
console.log("[Chain Workbench] Calling insertEdgeForward with zoneChoice:", zoneChoice);
|
||||
try {
|
||||
await insertEdgeForward(
|
||||
this.app,
|
||||
|
|
@ -649,25 +970,127 @@ export class ChainWorkbenchModal extends Modal {
|
|||
}
|
||||
|
||||
private async refreshWorkbench(): Promise<void> {
|
||||
// Close and reopen workbench to refresh
|
||||
this.close();
|
||||
|
||||
// Re-run command
|
||||
const { executeChainWorkbench } = await import("../commands/chainWorkbenchCommand");
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
const activeEditor = this.app.workspace.activeEditor?.editor;
|
||||
try {
|
||||
// Reload the workbench model without closing the modal
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
const activeEditor = this.app.workspace.activeEditor?.editor;
|
||||
|
||||
if (activeFile && activeEditor) {
|
||||
await executeChainWorkbench(
|
||||
if (!activeFile || !activeEditor) {
|
||||
new Notice("No active file or editor");
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve section context
|
||||
const { resolveSectionContext } = await import("../analysis/sectionContext");
|
||||
const context = resolveSectionContext(activeEditor, activeFile.path);
|
||||
|
||||
// Build inspector options
|
||||
const inspectorOptions = {
|
||||
includeNoteLinks: true,
|
||||
includeCandidates: this.settings.chainInspectorIncludeCandidates,
|
||||
maxDepth: 3,
|
||||
direction: "both" as const,
|
||||
maxTemplateMatches: undefined,
|
||||
maxMatchesPerTemplateDefault: this.settings.maxMatchesPerTemplateDefault,
|
||||
debugLogging: this.settings.debugLogging,
|
||||
};
|
||||
|
||||
// Load vocabulary
|
||||
const { VocabularyLoader } = await import("../vocab/VocabularyLoader");
|
||||
const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary");
|
||||
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
||||
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
||||
|
||||
// Inspect chains
|
||||
const { inspectChains } = await import("../analysis/chainInspector");
|
||||
const report = await inspectChains(
|
||||
this.app,
|
||||
activeEditor,
|
||||
activeFile.path,
|
||||
context,
|
||||
inspectorOptions,
|
||||
this.chainRoles,
|
||||
this.settings.edgeVocabularyPath,
|
||||
this.chainTemplates,
|
||||
undefined,
|
||||
this.settings,
|
||||
this.pluginInstance
|
||||
this.settings.templateMatchingProfile
|
||||
);
|
||||
|
||||
// Build all edges index
|
||||
const { buildNoteIndex } = await import("../analysis/graphIndex");
|
||||
const fileObj = this.app.vault.getAbstractFileByPath(activeFile.path);
|
||||
if (!fileObj || !(fileObj instanceof TFile)) {
|
||||
throw new Error("Active file not found");
|
||||
}
|
||||
const { edges: allEdges } = await buildNoteIndex(this.app, fileObj);
|
||||
|
||||
// Build workbench model
|
||||
const { buildWorkbenchModel } = await import("../workbench/workbenchBuilder");
|
||||
const newModel = await buildWorkbenchModel(
|
||||
this.app,
|
||||
report,
|
||||
this.chainTemplates,
|
||||
this.chainRoles,
|
||||
edgeVocabulary,
|
||||
allEdges,
|
||||
this.settings.debugLogging
|
||||
);
|
||||
|
||||
// Update model and re-render
|
||||
this.model = newModel;
|
||||
this.selectedMatch = null; // Reset selection
|
||||
this.filterStatus = null; // Reset filters
|
||||
this.searchQuery = ""; // Reset search
|
||||
|
||||
// Clear and re-render the entire UI
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
// Rebuild the UI structure
|
||||
const header = contentEl.createDiv({ cls: "workbench-header" });
|
||||
header.createEl("h2", { text: "Chain Workbench" });
|
||||
header.createEl("p", {
|
||||
cls: "context-info",
|
||||
text: `Context: ${this.model.context.file}${this.model.context.heading ? `#${this.model.context.heading}` : ""}`,
|
||||
});
|
||||
|
||||
// Filters
|
||||
const filterContainer = contentEl.createDiv({ cls: "workbench-filters" });
|
||||
filterContainer.createEl("label", { text: "Filter by Status:" });
|
||||
const statusSelect = filterContainer.createEl("select");
|
||||
statusSelect.createEl("option", { text: "All", value: "" });
|
||||
statusSelect.createEl("option", { text: "Complete", value: "complete" });
|
||||
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
|
||||
statusSelect.createEl("option", { text: "Partial", value: "partial" });
|
||||
statusSelect.createEl("option", { text: "Weak", value: "weak" });
|
||||
statusSelect.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
this.filterStatus = target.value || null;
|
||||
this.render();
|
||||
});
|
||||
|
||||
filterContainer.createEl("label", { text: "Search:" });
|
||||
const searchInput = filterContainer.createEl("input", { type: "text", placeholder: "Template name..." });
|
||||
searchInput.addEventListener("input", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this.searchQuery = target.value.toLowerCase();
|
||||
this.render();
|
||||
});
|
||||
|
||||
// Main container: two-column layout
|
||||
const mainContainer = contentEl.createDiv({ cls: "workbench-main" });
|
||||
|
||||
// Left: Tree View
|
||||
const treeContainer = mainContainer.createDiv({ cls: "workbench-tree" });
|
||||
treeContainer.createEl("h3", { text: "Templates & Chains" });
|
||||
|
||||
// Right: Details View
|
||||
const detailsContainer = mainContainer.createDiv({ cls: "workbench-details" });
|
||||
detailsContainer.createEl("h3", { text: "Chain Details" });
|
||||
|
||||
// Now render the content
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error("[Chain Workbench] Error refreshing workbench:", error);
|
||||
new Notice(`Error refreshing workbench: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2592,6 +2592,7 @@ export class InterviewWizardModal extends Modal {
|
|||
// WP-26: Übersichts-Modal immer anzeigen, wenn Vocabulary geladen ist (einmal alle Kanten prüfen/ändern).
|
||||
// Verhindert die langsame Schritt-für-Schritt-Bearbeitung; Standardkanten können unverändert übernommen werden.
|
||||
let sectionEdgeTypes: Map<string, Map<string, string>> | undefined = undefined;
|
||||
let noteEdgesFromModal: Map<string, Map<string, string>> | undefined = undefined;
|
||||
if (this.vocabulary) {
|
||||
try {
|
||||
const graphSchema = this.plugin?.ensureGraphSchemaLoaded
|
||||
|
|
@ -2612,6 +2613,7 @@ export class InterviewWizardModal extends Modal {
|
|||
// Bei OK immer übernehmen (auch wenn nichts geändert wurde), damit Renderer korrekte Types/Inversen nutzt
|
||||
if (!result.cancelled) {
|
||||
sectionEdgeTypes = result.sectionEdges;
|
||||
noteEdgesFromModal = result.noteEdges;
|
||||
console.log("[WP-26] Edge-Types aus Übersicht übernommen:", {
|
||||
sectionEdgesCount: result.sectionEdges.size,
|
||||
noteEdgesCount: result.noteEdges.size,
|
||||
|
|
@ -2651,7 +2653,7 @@ export class InterviewWizardModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
await this.applyPatches(sectionEdgeTypes);
|
||||
await this.applyPatches(sectionEdgeTypes, noteEdgesFromModal);
|
||||
|
||||
// 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
|
||||
|
|
@ -3240,7 +3242,10 @@ export class InterviewWizardModal extends Modal {
|
|||
};
|
||||
}
|
||||
|
||||
async applyPatches(sectionEdgeTypes?: Map<string, Map<string, string>>): Promise<void> {
|
||||
async applyPatches(
|
||||
sectionEdgeTypes?: Map<string, Map<string, string>>,
|
||||
noteEdges?: Map<string, Map<string, string>>
|
||||
): Promise<void> {
|
||||
console.log("=== APPLY PATCHES ===", {
|
||||
patchCount: this.state.patches.length,
|
||||
patches: this.state.patches.map(p => ({
|
||||
|
|
@ -3313,12 +3318,13 @@ export class InterviewWizardModal extends Modal {
|
|||
sectionSequence: Array.from(this.state.sectionSequence), // WP-26: Section-Sequenz übergeben
|
||||
};
|
||||
|
||||
// WP-26: Render-Optionen für Section-Types und Edge-Vorschläge
|
||||
// WP-26: Render-Optionen für Section-Types, Edge-Vorschläge und Note-Edges (inkl. [[Note#Abschnitt]])
|
||||
const renderOptions: RenderOptions = {
|
||||
graphSchema: graphSchema,
|
||||
vocabulary: vocabulary,
|
||||
noteType: this.state.profile.note_type,
|
||||
sectionEdgeTypes: sectionEdgeTypes, // WP-26: Manuell geänderte Edge-Types vom Übersichts-Modal
|
||||
sectionEdgeTypes: sectionEdgeTypes,
|
||||
noteEdges: noteEdges,
|
||||
};
|
||||
|
||||
// WP-26: Debug-Log für Section-Sequenz
|
||||
|
|
|
|||
|
|
@ -336,6 +336,25 @@ export class MindnetSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
// Max matches per template (default)
|
||||
new Setting(containerEl)
|
||||
.setName("Max matches per template (default)")
|
||||
.setDesc(
|
||||
"Standard: Wie viele verschiedene Zuordnungen pro Template maximal berücksichtigt werden (z. B. intra-note + cross-note). 1–10. Wird durch chain_templates.yaml (defaults.matching.max_matches_per_template) überschrieben, falls dort gesetzt."
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("2")
|
||||
.setValue(String(this.plugin.settings.maxMatchesPerTemplateDefault))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue >= 1 && numValue <= 10) {
|
||||
this.plugin.settings.maxMatchesPerTemplateDefault = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 2. Graph Traversal & Linting
|
||||
// ============================================
|
||||
|
|
@ -803,7 +822,75 @@ export class MindnetSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
// ============================================
|
||||
// 8. Debug & Development
|
||||
// 8. Backend Logging Configuration
|
||||
// ============================================
|
||||
containerEl.createEl("h3", { text: "📝 Backend Logging" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Konfiguration für das Backend-Logging. Steuert Log-Level, Dateigröße und Rotation. Diese Einstellungen werden beim Backend-Start verwendet.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
// Log level
|
||||
new Setting(containerEl)
|
||||
.setName("Log level")
|
||||
.setDesc(
|
||||
"Log-Level für das Backend: 'INFO' (Standard, weniger Zeilen), 'WARNING' (nur Warnungen/Fehler), 'ERROR' (nur Fehler), oder 'DEBUG' (sehr ausführlich, viele Zeilen)."
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("INFO", "INFO")
|
||||
.addOption("WARNING", "WARNING")
|
||||
.addOption("ERROR", "ERROR")
|
||||
.addOption("DEBUG", "DEBUG")
|
||||
.setValue(this.plugin.settings.logLevel)
|
||||
.onChange(async (value) => {
|
||||
if (value === "DEBUG" || value === "INFO" || value === "WARNING" || value === "ERROR") {
|
||||
this.plugin.settings.logLevel = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Log max bytes
|
||||
new Setting(containerEl)
|
||||
.setName("Log max bytes")
|
||||
.setDesc(
|
||||
"Maximale Größe einer Log-Datei in Bytes vor Rotation. Standard: 1048576 (1 MB). Kleinere Werte führen zu häufigerer Rotation und kürzeren Einzeldateien. Empfohlen: 524288 (512 KB) für sehr große Logs."
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("1048576")
|
||||
.setValue(String(this.plugin.settings.logMaxBytes))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
this.plugin.settings.logMaxBytes = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Log backup count
|
||||
new Setting(containerEl)
|
||||
.setName("Log backup count")
|
||||
.setDesc(
|
||||
"Anzahl rotierter Backup-Dateien für logs/mindnet.log. Standard: 2. Bei Erreichen von 'Log max bytes' wird die aktuelle Datei rotiert (mindnet.log.1, mindnet.log.2, etc.)."
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("2")
|
||||
.setValue(String(this.plugin.settings.logBackupCount))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue >= 0) {
|
||||
this.plugin.settings.logBackupCount = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 9. Debug & Development
|
||||
// ============================================
|
||||
containerEl.createEl("h3", { text: "🐛 Debug & Development" });
|
||||
containerEl.createEl("p", {
|
||||
|
|
@ -827,7 +914,84 @@ export class MindnetSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
// ============================================
|
||||
// 9. Export
|
||||
// 9.1. Module-specific Logging
|
||||
// ============================================
|
||||
containerEl.createEl("h3", { text: "📊 Modulspezifisches Logging" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Konfigurieren Sie individuelle Log-Level für verschiedene Module. Nützlich zum gezielten Debugging von Chain-Matching und Edge-Erkennung. Logs erscheinen in der Browser-Konsole (F12).",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
// Ensure moduleLogLevels exists
|
||||
if (!this.plugin.settings.moduleLogLevels) {
|
||||
this.plugin.settings.moduleLogLevels = {};
|
||||
}
|
||||
|
||||
// Define important modules for chain matching
|
||||
const importantModules = [
|
||||
{ name: "templateMatching", description: "Template-Matching: Erkennt Chain-Templates und ordnet Slots zu" },
|
||||
{ name: "chainInspector", description: "Chain Inspector: Analysiert Chains und findet fehlende Kanten" },
|
||||
{ name: "todoGenerator", description: "Todo Generator: Generiert Todos für fehlende Slots und Links" },
|
||||
{ name: "graphIndex", description: "Graph Index: Baut den Graph-Index auf und verwaltet Edges" },
|
||||
{ name: "workbenchBuilder", description: "Workbench Builder: Baut Workbench-Matches auf" },
|
||||
{ name: "chainWorkbenchCommand", description: "Chain Workbench Command: Hauptkommando für Chain-Workbench" },
|
||||
];
|
||||
|
||||
// Create settings for each module
|
||||
for (const module of importantModules) {
|
||||
const currentLevel = this.plugin.settings.moduleLogLevels[module.name] || "INFO";
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(`${module.name}`)
|
||||
.setDesc(module.description)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("NONE", "NONE (keine Logs)")
|
||||
.addOption("ERROR", "ERROR (nur Fehler)")
|
||||
.addOption("WARN", "WARN (Warnungen und Fehler)")
|
||||
.addOption("INFO", "INFO (Standard)")
|
||||
.addOption("DEBUG", "DEBUG (sehr ausführlich)")
|
||||
.setValue(currentLevel)
|
||||
.onChange(async (value) => {
|
||||
if (value === "NONE" || value === "ERROR" || value === "WARN" || value === "INFO" || value === "DEBUG") {
|
||||
if (value === "INFO") {
|
||||
// Remove from config if set to default
|
||||
delete this.plugin.settings.moduleLogLevels[module.name];
|
||||
} else {
|
||||
this.plugin.settings.moduleLogLevels[module.name] = value;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Update logger registry immediately
|
||||
const { initializeLogging } = await import("../utils/logger");
|
||||
initializeLogging(this.plugin.settings.moduleLogLevels);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add button to reset all to defaults
|
||||
new Setting(containerEl)
|
||||
.setName("Alle auf Standard zurücksetzen")
|
||||
.setDesc("Setzt alle Modul-Log-Level auf INFO (Standard) zurück.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Zurücksetzen")
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.moduleLogLevels = {};
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Update logger registry
|
||||
const { initializeLogging } = await import("../utils/logger");
|
||||
initializeLogging({});
|
||||
|
||||
// Refresh settings UI
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// 10. Export
|
||||
// ============================================
|
||||
containerEl.createEl("h3", { text: "📤 Export" });
|
||||
containerEl.createEl("p", {
|
||||
|
|
@ -883,7 +1047,7 @@ export class MindnetSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
// ============================================
|
||||
// 5. Fix Actions Settings
|
||||
// 11. Fix Actions Settings
|
||||
// ============================================
|
||||
containerEl.createEl("h3", { text: "🔧 Fix Actions" });
|
||||
containerEl.createEl("p", {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,37 @@
|
|||
|
||||
import { App, TFile } from "obsidian";
|
||||
|
||||
/**
|
||||
* Normalize heading string for comparison only (Inspect Chains / Chain Workbench).
|
||||
* Maps "Überschrift", "Überschrift ^Block", "Überschrift Block" to the same canonical form
|
||||
* so that Obsidian UI links (no ^) and plugin/sectionParser variants match.
|
||||
* - Strip trailing block-id with caret: \s+\^[a-zA-Z0-9_-]+$
|
||||
* - Strip one trailing word (Obsidian stores "Überschrift Block" without ^)
|
||||
* Use only for equality checks, not for display or stored links.
|
||||
*/
|
||||
export function normalizeHeadingForMatch(heading: string | null): string | null {
|
||||
if (heading === null || heading === undefined) return null;
|
||||
let s = heading.trim();
|
||||
if (!s) return null;
|
||||
// 1) Remove optional block-id suffix with caret: "Überschrift ^Block" -> "Überschrift"
|
||||
s = s.replace(/\s+\^[a-zA-Z0-9_-]+\s*$/, "").trim();
|
||||
// 2) Remove one trailing word (Obsidian link form "Überschrift Block" -> "Überschrift")
|
||||
s = s.replace(/\s+[a-zA-Z0-9_-]+\s*$/, "").trim();
|
||||
return s || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two headings for equality using normalized form (block-id suffix and trailing word stripped).
|
||||
* Use for Inspect Chains / Chain Workbench matching only.
|
||||
*/
|
||||
export function headingsMatch(a: string | null, b: string | null): boolean {
|
||||
const na = normalizeHeadingForMatch(a);
|
||||
const nb = normalizeHeadingForMatch(b);
|
||||
if (na === null && nb === null) return true;
|
||||
if (na === null || nb === null) return false;
|
||||
return na === nb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize link target by removing alias separator (|) and heading separator (#).
|
||||
* Preserves spaces and case.
|
||||
|
|
|
|||
192
src/utils/logger.ts
Normal file
192
src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* Modular logging system with module-specific log levels.
|
||||
* Allows fine-grained control over which modules log what level of messages.
|
||||
*/
|
||||
|
||||
export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "NONE";
|
||||
|
||||
export interface ModuleLogLevels {
|
||||
[moduleName: string]: LogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default log level for modules that don't have a specific configuration.
|
||||
*/
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = "INFO";
|
||||
|
||||
/**
|
||||
* Log level hierarchy (higher number = more verbose)
|
||||
*/
|
||||
const LOG_LEVEL_HIERARCHY: Record<LogLevel, number> = {
|
||||
NONE: 0,
|
||||
ERROR: 1,
|
||||
WARN: 2,
|
||||
INFO: 3,
|
||||
DEBUG: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* Logger class for module-specific logging.
|
||||
*/
|
||||
export class Logger {
|
||||
private moduleName: string;
|
||||
|
||||
constructor(moduleName: string) {
|
||||
this.moduleName = moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current log level for this module from registry.
|
||||
*/
|
||||
private getLogLevel(): LogLevel {
|
||||
const level = loggerRegistry.getLogLevel(this.moduleName);
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a log level should be output for this module.
|
||||
*/
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
const moduleLevel = this.getLogLevel();
|
||||
const hierarchy = LOG_LEVEL_HIERARCHY[moduleLevel];
|
||||
const requiredHierarchy = LOG_LEVEL_HIERARCHY[level];
|
||||
return hierarchy >= requiredHierarchy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format log message with module name and timestamp.
|
||||
*/
|
||||
private formatMessage(level: LogLevel, ...args: unknown[]): string[] {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = `[${timestamp}] [${this.moduleName}] [${level}]`;
|
||||
return [prefix, ...args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)))];
|
||||
}
|
||||
|
||||
debug(...args: unknown[]): void {
|
||||
if (this.shouldLog("DEBUG")) {
|
||||
// Use console.log instead of console.debug because some browsers filter console.debug
|
||||
// The log level is already indicated in the formatted message
|
||||
console.log(...this.formatMessage("DEBUG", ...args));
|
||||
}
|
||||
}
|
||||
|
||||
info(...args: unknown[]): void {
|
||||
if (this.shouldLog("INFO")) {
|
||||
console.info(...this.formatMessage("INFO", ...args));
|
||||
}
|
||||
}
|
||||
|
||||
warn(...args: unknown[]): void {
|
||||
if (this.shouldLog("WARN")) {
|
||||
console.warn(...this.formatMessage("WARN", ...args));
|
||||
}
|
||||
}
|
||||
|
||||
error(...args: unknown[]): void {
|
||||
if (this.shouldLog("ERROR")) {
|
||||
console.error(...this.formatMessage("ERROR", ...args));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a structured object with a message (useful for debugging complex data).
|
||||
*/
|
||||
debugObject(message: string, obj: unknown): void {
|
||||
if (this.shouldLog("DEBUG")) {
|
||||
console.debug(...this.formatMessage("DEBUG", message), obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log edge matching details (specific to chain matching debugging).
|
||||
*/
|
||||
debugEdgeMatch(message: string, details: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
edgeType?: string;
|
||||
templateName?: string;
|
||||
slotId?: string;
|
||||
matched?: boolean;
|
||||
reason?: string;
|
||||
}): void {
|
||||
if (this.shouldLog("DEBUG")) {
|
||||
console.debug(...this.formatMessage("DEBUG", message), details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log template matching details.
|
||||
*/
|
||||
debugTemplateMatch(message: string, details: {
|
||||
templateName?: string;
|
||||
score?: number;
|
||||
slotsComplete?: boolean;
|
||||
linksComplete?: boolean;
|
||||
missingSlots?: string[];
|
||||
satisfiedLinks?: number;
|
||||
requiredLinks?: number;
|
||||
}): void {
|
||||
if (this.shouldLog("DEBUG")) {
|
||||
console.debug(...this.formatMessage("DEBUG", message), details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global registry of module loggers.
|
||||
*/
|
||||
class LoggerRegistry {
|
||||
private moduleLogLevels: ModuleLogLevels = {};
|
||||
private loggers: Map<string, Logger> = new Map();
|
||||
|
||||
/**
|
||||
* Update log levels for all modules.
|
||||
*/
|
||||
updateLogLevels(levels: ModuleLogLevels): void {
|
||||
this.moduleLogLevels = { ...levels };
|
||||
// Invalidate existing loggers so they pick up new levels
|
||||
this.loggers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a logger for a module.
|
||||
*/
|
||||
getLogger(moduleName: string): Logger {
|
||||
if (!this.loggers.has(moduleName)) {
|
||||
this.loggers.set(moduleName, new Logger(moduleName));
|
||||
}
|
||||
return this.loggers.get(moduleName)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current log level for a module.
|
||||
*/
|
||||
getLogLevel(moduleName: string): LogLevel {
|
||||
return this.moduleLogLevels[moduleName] || DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured module log levels.
|
||||
*/
|
||||
getAllLogLevels(): ModuleLogLevels {
|
||||
return { ...this.moduleLogLevels };
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const loggerRegistry = new LoggerRegistry();
|
||||
|
||||
/**
|
||||
* Convenience function to get a logger for a module.
|
||||
* Usage: const logger = getLogger("templateMatching");
|
||||
*/
|
||||
export function getLogger(moduleName: string): Logger {
|
||||
return loggerRegistry.getLogger(moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize logger registry with settings.
|
||||
*/
|
||||
export function initializeLogging(moduleLogLevels: ModuleLogLevels): void {
|
||||
loggerRegistry.updateLogLevels(moduleLogLevels);
|
||||
}
|
||||
149
src/workbench/insertEdgeIntoSectionContent.ts
Normal file
149
src/workbench/insertEdgeIntoSectionContent.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Pure logic: compute section content after inserting an edge into the abstract block.
|
||||
* Used by insertEdgeInSection and by tests.
|
||||
*/
|
||||
|
||||
import { extractExistingMappings } from "../mapping/mappingExtractor";
|
||||
import { buildMappingBlock, insertMappingBlock } from "../mapping/mappingBuilder";
|
||||
|
||||
export interface InsertEdgeOptions {
|
||||
wrapperCalloutType: string;
|
||||
wrapperTitle: string;
|
||||
wrapperFolded: boolean;
|
||||
}
|
||||
|
||||
/** Separator between edge blocks inside abstract: exactly one line with exactly one ">". */
|
||||
const EDGE_GROUP_SEPARATOR = "\n>\n";
|
||||
|
||||
/**
|
||||
* Compute new section content after inserting one edge (edgeType → targetLink).
|
||||
* - If section has an abstract block: insert into it (append to same edge-type group, or add new group separated by ">").
|
||||
* - If no abstract block: append new mapping block at end.
|
||||
*/
|
||||
export function computeSectionContentAfterInsertEdge(
|
||||
sectionContent: string,
|
||||
edgeType: string,
|
||||
targetLink: string,
|
||||
options: InsertEdgeOptions
|
||||
): string {
|
||||
const { wrapperCalloutType, wrapperTitle, wrapperFolded } = options;
|
||||
const mappingState = extractExistingMappings(sectionContent, wrapperCalloutType, wrapperTitle);
|
||||
const hasMappingBlock = mappingState.wrapperBlockStartLine !== null && mappingState.wrapperBlockEndLine !== null;
|
||||
|
||||
if (hasMappingBlock && mappingState.wrapperBlockStartLine !== null && mappingState.wrapperBlockEndLine !== null) {
|
||||
const sectionLines = sectionContent.split(/\r?\n/);
|
||||
const wrapperStart = mappingState.wrapperBlockStartLine;
|
||||
const wrapperEnd = mappingState.wrapperBlockEndLine;
|
||||
const wrapperBlockContent = sectionLines.slice(wrapperStart, wrapperEnd).join("\n");
|
||||
|
||||
// Same edge type already exists → append target to that group
|
||||
// Match includes the separator ">\n" if present before next edge type
|
||||
const edgeTypeGroupRegex = new RegExp(
|
||||
`(>>\\s*\\[!edge\\]\\s+${edgeType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?)(?=\\n\\n|\\n>>\\s*\\[!edge\\]|\\n>\\s*\\^map-|$)`
|
||||
);
|
||||
const edgeTypeMatch = wrapperBlockContent.match(edgeTypeGroupRegex);
|
||||
|
||||
if (edgeTypeMatch && edgeTypeMatch[1]) {
|
||||
// Get the matched group
|
||||
const matchedGroup = edgeTypeMatch[1];
|
||||
|
||||
// Check what comes after the matched group in the original content
|
||||
const matchEndIndex = edgeTypeMatch.index! + edgeTypeMatch[0].length;
|
||||
const afterMatch = wrapperBlockContent.slice(matchEndIndex);
|
||||
|
||||
// Check if there's a separator ">\n" immediately after the group
|
||||
const hasSeparatorAfter = afterMatch.match(/^\n>\s*\n/);
|
||||
|
||||
// Also check if the group itself ends with a separator
|
||||
const trimmedGroup = matchedGroup.trimEnd();
|
||||
const endsWithSeparator = trimmedGroup.endsWith("\n>");
|
||||
const endsWithTarget = trimmedGroup.match(/>>\s*\[\[[^\]]+\]\]\s*$/);
|
||||
|
||||
let updatedGroup: string;
|
||||
let updatedAfterMatch = afterMatch;
|
||||
|
||||
if (endsWithSeparator || hasSeparatorAfter) {
|
||||
// Group has a separator (either at end or after), insert new target before separator
|
||||
if (endsWithSeparator) {
|
||||
// Separator is part of the group, remove it, add target, restore separator
|
||||
const separatorIndex = trimmedGroup.lastIndexOf("\n>");
|
||||
let groupWithoutSeparator: string;
|
||||
if (separatorIndex >= 0) {
|
||||
// Remove separator and everything after it
|
||||
groupWithoutSeparator = trimmedGroup.slice(0, separatorIndex);
|
||||
// Remove trailing whitespace but preserve the newline structure
|
||||
// We want to keep exactly one newline before adding the new target
|
||||
groupWithoutSeparator = groupWithoutSeparator.replace(/[ \t]+$/, ""); // Remove trailing spaces/tabs only
|
||||
} else {
|
||||
groupWithoutSeparator = trimmedGroup.replace(/\n>\s*$/, "").trimEnd();
|
||||
}
|
||||
// Add target directly followed by separator (>\n), no blank line in between
|
||||
// The separator should be "\n>\n" (one line with ">", then newline for next group)
|
||||
// But we need to ensure no extra blank line after the separator
|
||||
updatedGroup = groupWithoutSeparator + "\n>> [[" + targetLink + "]]\n>";
|
||||
} else {
|
||||
// Separator is after the group, add target before it
|
||||
// Ensure no blank line between target and separator
|
||||
updatedGroup = trimmedGroup.trimEnd() + "\n>> [[" + targetLink + "]]";
|
||||
// Remove any blank lines before separator in afterMatch
|
||||
updatedAfterMatch = afterMatch.replace(/^\s*\n+/, "\n");
|
||||
}
|
||||
} else if (endsWithTarget) {
|
||||
// Group ends with a target, append new target
|
||||
updatedGroup = trimmedGroup + "\n>> [[" + targetLink + "]]\n";
|
||||
} else {
|
||||
// Fallback: append with newline
|
||||
updatedGroup = trimmedGroup + "\n>> [[" + targetLink + "]]\n";
|
||||
}
|
||||
|
||||
// Ensure no blank line between separator and next edge type
|
||||
// The separator ends with ">\n", and the next edge type should come immediately after (no blank line)
|
||||
// Remove any blank lines (multiple newlines) between separator and next edge type
|
||||
let cleanedAfterMatch = updatedAfterMatch;
|
||||
// If afterMatch starts with newline(s) followed by >> [!edge], ensure only one newline
|
||||
if (cleanedAfterMatch.match(/^\n+>>\s*\[!edge\]/)) {
|
||||
// Replace multiple newlines with single newline
|
||||
cleanedAfterMatch = cleanedAfterMatch.replace(/^(\n+)(>>\s*\[!edge\])/, "\n$2");
|
||||
}
|
||||
|
||||
const updatedWrapperBlock = wrapperBlockContent.slice(0, edgeTypeMatch.index!) + updatedGroup + cleanedAfterMatch;
|
||||
const beforeWrapper = sectionLines.slice(0, wrapperStart).join("\n");
|
||||
const afterWrapper = sectionLines.slice(wrapperEnd).join("\n");
|
||||
return beforeWrapper + "\n" + updatedWrapperBlock + "\n" + afterWrapper;
|
||||
}
|
||||
|
||||
// New edge type → add new group, separated by exactly one line with exactly one ">"
|
||||
const blockIdMatch = wrapperBlockContent.match(/\n>?\s*\^map-/);
|
||||
const insertPos = blockIdMatch ? blockIdMatch.index ?? wrapperBlockContent.length : wrapperBlockContent.length;
|
||||
const endsWithNewline = insertPos > 0 && wrapperBlockContent[insertPos - 1] === "\n";
|
||||
const newEdgeGroup =
|
||||
(endsWithNewline ? ">\n" : EDGE_GROUP_SEPARATOR) + `>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
|
||||
const updatedWrapperBlock =
|
||||
wrapperBlockContent.slice(0, insertPos) + newEdgeGroup + wrapperBlockContent.slice(insertPos);
|
||||
const beforeWrapper = sectionLines.slice(0, wrapperStart).join("\n");
|
||||
const afterWrapper = sectionLines.slice(wrapperEnd).join("\n");
|
||||
return beforeWrapper + "\n" + updatedWrapperBlock + "\n" + afterWrapper;
|
||||
}
|
||||
|
||||
// No abstract block: append new mapping block at end (use full targetLink for display)
|
||||
const targetLinkNorm = targetLink.split("|")[0]?.trim() || targetLink;
|
||||
const existingMappings = new Map<string, string>();
|
||||
existingMappings.set(targetLinkNorm, edgeType);
|
||||
const foldMarker = wrapperFolded ? "-" : "+";
|
||||
const newMappingBlock = buildMappingBlock(
|
||||
[targetLinkNorm],
|
||||
existingMappings,
|
||||
{
|
||||
wrapperCalloutType,
|
||||
wrapperTitle,
|
||||
wrapperFolded,
|
||||
defaultEdgeType: "",
|
||||
assignUnmapped: "none",
|
||||
}
|
||||
);
|
||||
if (newMappingBlock) {
|
||||
return insertMappingBlock(sectionContent, newMappingBlock);
|
||||
}
|
||||
const mappingBlock = `\n\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
|
||||
return sectionContent.trimEnd() + mappingBlock;
|
||||
}
|
||||
|
|
@ -20,7 +20,14 @@ import type {
|
|||
OneSidedConnectivityTodo,
|
||||
} from "./types";
|
||||
import { resolveCanonicalEdgeType } from "../analysis/chainInspector";
|
||||
import { headingsMatch } from "../unresolvedLink/linkHelpers";
|
||||
import type { EdgeVocabulary } from "../vocab/types";
|
||||
import { getLogger } from "../utils/logger";
|
||||
|
||||
// Don't create logger instance at import time - create it when needed
|
||||
function getTodoGeneratorLogger() {
|
||||
return getLogger("todoGenerator");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate todos for a template match.
|
||||
|
|
@ -34,7 +41,8 @@ export async function generateTodos(
|
|||
allEdges: IndexedEdge[],
|
||||
candidateEdges: IndexedEdge[],
|
||||
confirmedEdges: IndexedEdge[],
|
||||
context: { file: string; heading: string | null }
|
||||
context: { file: string; heading: string | null },
|
||||
debugLogging?: boolean
|
||||
): Promise<WorkbenchTodoUnion[]> {
|
||||
const todos: WorkbenchTodoUnion[] = [];
|
||||
|
||||
|
|
@ -80,10 +88,51 @@ export async function generateTodos(
|
|||
ev.from === link.from && ev.to === link.to
|
||||
);
|
||||
|
||||
console.log(`[todoGenerator] Checking link ${link.from} -> ${link.to}:`, {
|
||||
linkExists,
|
||||
roleEvidence: match.roleEvidence?.filter(ev => ev.from === link.from && ev.to === link.to)
|
||||
// Log edge matching details for debugging
|
||||
getTodoGeneratorLogger().debugEdgeMatch(`Checking link ${link.from} -> ${link.to}`, {
|
||||
from: link.from,
|
||||
to: link.to,
|
||||
matched: linkExists,
|
||||
reason: linkExists ? "Found in roleEvidence" : "Not found in roleEvidence"
|
||||
});
|
||||
|
||||
if (!linkExists) {
|
||||
const fromNode = match.slotAssignments[link.from];
|
||||
const toNode = match.slotAssignments[link.to];
|
||||
const matchingRoleEvidence = match.roleEvidence?.filter(ev => ev.from === link.from && ev.to === link.to);
|
||||
|
||||
getTodoGeneratorLogger().debugObject(`🔍 Missing link ${link.from} -> ${link.to}`, {
|
||||
linkExists,
|
||||
fromNode: fromNode ? `${fromNode.file}#${fromNode.heading || ""}` : "MISSING",
|
||||
toNode: toNode ? `${toNode.file}#${toNode.heading || ""}` : "MISSING",
|
||||
roleEvidence: matchingRoleEvidence,
|
||||
allRoleEvidence: match.roleEvidence,
|
||||
allowedEdgeRoles: link.allowed_edge_roles
|
||||
});
|
||||
|
||||
// Check what edges exist between these nodes
|
||||
if (fromNode && toNode) {
|
||||
const fromKey = `${fromNode.file}:${fromNode.heading || ""}`;
|
||||
const toKey = `${toNode.file}:${toNode.heading || ""}`;
|
||||
const edgesBetween = allEdges.filter(e => {
|
||||
const eFrom = "sectionHeading" in e.source ? `${e.source.file}:${e.source.sectionHeading || ""}` : `${e.source.file}:`;
|
||||
const eTo = `${e.target.file}:${e.target.heading || ""}`;
|
||||
return eFrom === fromKey && eTo === toKey;
|
||||
});
|
||||
|
||||
getTodoGeneratorLogger().debugObject(`📊 Edges between nodes`, {
|
||||
fromKey,
|
||||
toKey,
|
||||
edgesFound: edgesBetween.length,
|
||||
edges: edgesBetween.map(e => ({
|
||||
type: e.rawEdgeType,
|
||||
scope: e.scope,
|
||||
source: "sectionHeading" in e.source ? `${e.source.file}#${e.source.sectionHeading}` : `${e.source.file}`,
|
||||
target: `${e.target.file}#${e.target.heading || ""}`
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkExists) {
|
||||
// Get suggested edge types from chain_roles for allowed_edge_roles
|
||||
|
|
@ -136,12 +185,20 @@ export async function generateTodos(
|
|||
|
||||
if (allStructuralTemporal) {
|
||||
const weakEdges = match.roleEvidence.map((ev) => {
|
||||
// Find corresponding edge in allEdges
|
||||
// Parse ev.from / ev.to (format "file:heading") for normalized comparison
|
||||
const parseNodeKey = (key: string) => {
|
||||
const i = key.lastIndexOf(":");
|
||||
return i >= 0 ? { file: key.slice(0, i), heading: key.slice(i + 1) || null } : { file: key, heading: null };
|
||||
};
|
||||
const fromNode = parseNodeKey(ev.from);
|
||||
const toNode = parseNodeKey(ev.to);
|
||||
const edge = allEdges.find(
|
||||
(e) =>
|
||||
e.rawEdgeType === ev.rawEdgeType &&
|
||||
`${e.source.file}:${"sectionHeading" in e.source ? e.source.sectionHeading || "" : ""}` === ev.from &&
|
||||
`${e.target.file}:${e.target.heading || ""}` === ev.to
|
||||
e.source.file === fromNode.file &&
|
||||
headingsMatch("sectionHeading" in e.source ? e.source.sectionHeading : null, fromNode.heading) &&
|
||||
e.target.file === toNode.file &&
|
||||
headingsMatch(e.target.heading, toNode.heading)
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -193,17 +250,16 @@ export async function generateTodos(
|
|||
// Find matching candidate edge
|
||||
// Relation-Equality: same direction, same sourceRef/targetRef, canonical edgeType equal
|
||||
const matchingCandidate = candidateEdges.find((candidate) => {
|
||||
// Build keys for comparison
|
||||
const candidateFromKey = `${
|
||||
"sectionHeading" in candidate.source ? candidate.source.file : candidate.source.file
|
||||
}:${"sectionHeading" in candidate.source ? candidate.source.sectionHeading || "" : ""}`;
|
||||
const candidateToKey = `${candidate.target.file}:${candidate.target.heading || ""}`;
|
||||
const fromFile = "sectionHeading" in candidate.source ? candidate.source.file : candidate.source.file;
|
||||
const toFile = candidate.target.file;
|
||||
const fromHeading = "sectionHeading" in candidate.source ? candidate.source.sectionHeading : null;
|
||||
const toHeading = candidate.target.heading;
|
||||
|
||||
const requiredFromKey = `${missingLinkTodo.fromNodeRef.file}:${missingLinkTodo.fromNodeRef.heading || ""}`;
|
||||
const requiredToKey = `${missingLinkTodo.toNodeRef.file}:${missingLinkTodo.toNodeRef.heading || ""}`;
|
||||
|
||||
// Check direction and node refs match
|
||||
if (candidateFromKey !== requiredFromKey || candidateToKey !== requiredToKey) {
|
||||
// Check direction and node refs match (normalized heading comparison)
|
||||
if (fromFile !== missingLinkTodo.fromNodeRef.file || !headingsMatch(fromHeading, missingLinkTodo.fromNodeRef.heading)) {
|
||||
return false;
|
||||
}
|
||||
if (toFile !== missingLinkTodo.toNodeRef.file || !headingsMatch(toHeading, missingLinkTodo.toNodeRef.heading)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -266,17 +322,16 @@ function checkRelationExists(
|
|||
suggestedEdgeTypes: string[],
|
||||
edgeVocabulary: EdgeVocabulary | null
|
||||
): boolean {
|
||||
const fromKey = `${fromRef.file}:${fromRef.heading || ""}`;
|
||||
const toKey = `${toRef.file}:${toRef.heading || ""}`;
|
||||
|
||||
for (const edge of confirmedEdges) {
|
||||
// Check direction and node refs
|
||||
const edgeFromKey = `${
|
||||
"sectionHeading" in edge.source ? edge.source.file : edge.source.file
|
||||
}:${"sectionHeading" in edge.source ? edge.source.sectionHeading || "" : ""}`;
|
||||
const edgeToKey = `${edge.target.file}:${edge.target.heading || ""}`;
|
||||
const edgeFromFile = "sectionHeading" in edge.source ? edge.source.file : edge.source.file;
|
||||
const edgeFromHeading = "sectionHeading" in edge.source ? edge.source.sectionHeading : null;
|
||||
const edgeToFile = edge.target.file;
|
||||
const edgeToHeading = edge.target.heading;
|
||||
|
||||
if (edgeFromKey !== fromKey || edgeToKey !== toKey) {
|
||||
if (edgeFromFile !== fromRef.file || !headingsMatch(edgeFromHeading, fromRef.heading)) {
|
||||
continue;
|
||||
}
|
||||
if (edgeToFile !== toRef.file || !headingsMatch(edgeToHeading, toRef.heading)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +381,7 @@ export function generateFindingsTodos(
|
|||
// The finding evidence points to the source section
|
||||
const sourceMatches =
|
||||
"sectionHeading" in e.source
|
||||
? e.source.sectionHeading === finding.evidence?.sectionHeading &&
|
||||
? headingsMatch(e.source.sectionHeading, finding.evidence?.sectionHeading ?? null) &&
|
||||
e.source.file === finding.evidence?.file
|
||||
: e.source.file === finding.evidence?.file;
|
||||
return sourceMatches;
|
||||
|
|
@ -413,7 +468,7 @@ export function generateFindingsTodos(
|
|||
(e) =>
|
||||
e.scope === "section" &&
|
||||
("sectionHeading" in e.source
|
||||
? e.source.sectionHeading === context.heading && e.source.file === context.file
|
||||
? headingsMatch(e.source.sectionHeading, context.heading) && e.source.file === context.file
|
||||
: false)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ export async function scanVaultForChainGaps(
|
|||
maxDepth: 3,
|
||||
direction: "both" as const,
|
||||
maxTemplateMatches: undefined, // No limit
|
||||
maxMatchesPerTemplateDefault: settings.maxMatchesPerTemplateDefault,
|
||||
debugLogging: settings.debugLogging,
|
||||
};
|
||||
|
||||
// Prepare templates source info
|
||||
|
|
@ -152,7 +154,8 @@ export async function scanVaultForChainGaps(
|
|||
chainTemplates,
|
||||
chainRoles,
|
||||
edgeVocabulary,
|
||||
allEdges
|
||||
allEdges,
|
||||
settings.debugLogging
|
||||
);
|
||||
|
||||
// Skip if no matches
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ export async function buildWorkbenchModel(
|
|||
chainTemplates: ChainTemplatesConfig | null,
|
||||
chainRoles: ChainRolesConfig | null,
|
||||
edgeVocabulary: EdgeVocabulary | null,
|
||||
allEdges: IndexedEdge[]
|
||||
allEdges: IndexedEdge[],
|
||||
debugLogging?: boolean
|
||||
): Promise<WorkbenchModel> {
|
||||
const matches: WorkbenchMatch[] = [];
|
||||
|
||||
|
|
@ -73,7 +74,8 @@ export async function buildWorkbenchModel(
|
|||
allEdges,
|
||||
candidateEdges,
|
||||
confirmedEdges,
|
||||
report.context
|
||||
report.context,
|
||||
debugLogging
|
||||
);
|
||||
|
||||
matches.push({
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { EdgeTypeChooserModal } from "../ui/EdgeTypeChooserModal";
|
|||
import type { Vocabulary } from "../vocab/Vocabulary";
|
||||
import type { EdgeVocabulary } from "../vocab/types";
|
||||
import type { MindnetSettings } from "../settings";
|
||||
import { extractExistingMappings } from "../mapping/mappingExtractor";
|
||||
import { buildMappingBlock, insertMappingBlock } from "../mapping/mappingBuilder";
|
||||
import { computeSectionContentAfterInsertEdge } from "./insertEdgeIntoSectionContent";
|
||||
import { headingsMatch } from "../unresolvedLink/linkHelpers";
|
||||
|
||||
/**
|
||||
* Insert edge forward (from source to target).
|
||||
|
|
@ -30,15 +30,21 @@ export async function insertEdgeForward(
|
|||
graphSchema: any = null,
|
||||
targetZone: "section" | "note_links" | "candidates" = "section"
|
||||
): Promise<void> {
|
||||
console.log("[insertEdgeForward] ===== STARTING =====");
|
||||
console.log("[insertEdgeForward] targetZone:", targetZone);
|
||||
console.log("[insertEdgeForward] file:", file.path);
|
||||
console.log("[insertEdgeForward] todo type:", todo.type);
|
||||
console.log("[insertEdgeForward] Todo:", todo);
|
||||
console.log("[insertEdgeForward] Suggested edge types:", todo.suggestedEdgeTypes);
|
||||
const debugLogging = settings.debugLogging;
|
||||
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] ===== STARTING =====");
|
||||
console.log("[insertEdgeForward] targetZone:", targetZone);
|
||||
console.log("[insertEdgeForward] file:", file.path);
|
||||
console.log("[insertEdgeForward] todo type:", todo.type);
|
||||
console.log("[insertEdgeForward] Todo:", todo);
|
||||
console.log("[insertEdgeForward] Suggested edge types:", todo.suggestedEdgeTypes);
|
||||
}
|
||||
|
||||
// Let user choose edge type (direction is always "forward" for insert_edge_forward)
|
||||
console.log("[insertEdgeForward] Opening edge type chooser...");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Opening edge type chooser...");
|
||||
}
|
||||
const edgeType = await chooseEdgeType(
|
||||
app,
|
||||
edgeVocabulary,
|
||||
|
|
@ -46,12 +52,17 @@ export async function insertEdgeForward(
|
|||
todo.fromNodeRef.noteType,
|
||||
todo.toNodeRef.noteType,
|
||||
graphSchema,
|
||||
"forward" // insert_edge_forward is always forward direction
|
||||
"forward", // insert_edge_forward is always forward direction
|
||||
debugLogging
|
||||
);
|
||||
console.log("[insertEdgeForward] Selected edge type:", edgeType);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Selected edge type:", edgeType);
|
||||
}
|
||||
|
||||
if (!edgeType) {
|
||||
console.log("[insertEdgeForward] User cancelled edge type selection");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] User cancelled edge type selection");
|
||||
}
|
||||
return; // User cancelled
|
||||
}
|
||||
|
||||
|
|
@ -61,17 +72,23 @@ export async function insertEdgeForward(
|
|||
? `${targetBasename}#${todo.toNodeRef.heading}`
|
||||
: targetBasename;
|
||||
|
||||
console.log("[insertEdgeForward] Target link:", targetLink);
|
||||
console.log("[insertEdgeForward] Inserting into zone:", targetZone);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Target link:", targetLink);
|
||||
console.log("[insertEdgeForward] Inserting into zone:", targetZone);
|
||||
}
|
||||
|
||||
// Determine where to insert
|
||||
if (targetZone === "note_links") {
|
||||
// Insert in Note-Verbindungen zone
|
||||
console.log("[insertEdgeForward] Inserting into note_links zone");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Inserting into note_links zone");
|
||||
}
|
||||
await insertEdgeInZone(app, editor, file, "note_links", edgeType, targetLink, settings);
|
||||
} else if (targetZone === "candidates") {
|
||||
// Insert in Kandidaten zone
|
||||
console.log("[insertEdgeForward] Inserting into candidates zone");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Inserting into candidates zone");
|
||||
}
|
||||
await insertEdgeInZone(app, editor, file, "candidates", edgeType, targetLink, settings);
|
||||
} else {
|
||||
// Insert in source section - need to select section if multiple exist
|
||||
|
|
@ -98,14 +115,18 @@ export async function insertEdgeForward(
|
|||
);
|
||||
}
|
||||
|
||||
console.log("[insertEdgeForward] Trying to find source file, fileRef:", fileRef);
|
||||
console.log("[insertEdgeForward] Possible paths:", possiblePaths);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Trying to find source file, fileRef:", fileRef);
|
||||
console.log("[insertEdgeForward] Possible paths:", possiblePaths);
|
||||
}
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
const found = app.vault.getAbstractFileByPath(path);
|
||||
if (found && found instanceof TFile) {
|
||||
sourceFile = found;
|
||||
console.log("[insertEdgeForward] Found source file at path:", path);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Found source file at path:", path);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -113,18 +134,24 @@ export async function insertEdgeForward(
|
|||
// If still not found, try resolving as wikilink (like templateMatching does)
|
||||
if (!sourceFile) {
|
||||
const basename = fileRef.replace(/\.md$/, "").split("/").pop() || fileRef;
|
||||
console.log("[insertEdgeForward] Trying to resolve as wikilink, basename:", basename);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Trying to resolve as wikilink, basename:", basename);
|
||||
}
|
||||
const resolved = app.metadataCache.getFirstLinkpathDest(basename, file.path);
|
||||
if (resolved) {
|
||||
sourceFile = resolved;
|
||||
console.log("[insertEdgeForward] Found source file via wikilink resolution:", sourceFile.path);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Found source file via wikilink resolution:", sourceFile.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still not found, try searching by basename in all files
|
||||
if (!sourceFile) {
|
||||
const basename = fileRef.replace(/\.md$/, "").split("/").pop() || fileRef;
|
||||
console.log("[insertEdgeForward] Searching by basename in all files:", basename);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Searching by basename in all files:", basename);
|
||||
}
|
||||
const allFiles = app.vault.getMarkdownFiles();
|
||||
sourceFile = allFiles.find((f) => {
|
||||
const fBasename = f.basename;
|
||||
|
|
@ -132,7 +159,7 @@ export async function insertEdgeForward(
|
|||
return fBasename === basename || fPath.endsWith(`/${basename}.md`) || fPath === `${basename}.md`;
|
||||
}) || null;
|
||||
|
||||
if (sourceFile) {
|
||||
if (sourceFile && debugLogging) {
|
||||
console.log("[insertEdgeForward] Found source file by basename:", sourceFile.path);
|
||||
}
|
||||
}
|
||||
|
|
@ -143,16 +170,23 @@ export async function insertEdgeForward(
|
|||
return;
|
||||
}
|
||||
|
||||
console.log("[insertEdgeForward] Inserting into section, source file:", sourceFile.path, "preferred heading:", todo.fromNodeRef.heading);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Inserting into section, source file:", sourceFile.path, "preferred heading:", todo.fromNodeRef.heading);
|
||||
}
|
||||
const sourceSection = await selectSectionForEdge(
|
||||
app,
|
||||
sourceFile,
|
||||
todo.fromNodeRef.heading
|
||||
todo.fromNodeRef.heading,
|
||||
debugLogging
|
||||
);
|
||||
console.log("[insertEdgeForward] Selected section:", sourceSection);
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Selected section:", sourceSection);
|
||||
}
|
||||
|
||||
if (!sourceSection) {
|
||||
console.log("[insertEdgeForward] User cancelled section selection");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] User cancelled section selection");
|
||||
}
|
||||
return; // User cancelled
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +203,9 @@ export async function insertEdgeForward(
|
|||
sourceEditor = activeEditor;
|
||||
}
|
||||
|
||||
console.log("[insertEdgeForward] Inserting edge into section");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Inserting edge into section");
|
||||
}
|
||||
await insertEdgeInSection(
|
||||
app,
|
||||
sourceEditor,
|
||||
|
|
@ -179,7 +215,9 @@ export async function insertEdgeForward(
|
|||
targetLink,
|
||||
settings
|
||||
);
|
||||
console.log("[insertEdgeForward] Edge insertion completed");
|
||||
if (debugLogging) {
|
||||
console.log("[insertEdgeForward] Edge insertion completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,23 +337,32 @@ function findWrapperBlockEnd(
|
|||
async function selectSectionForEdge(
|
||||
app: App,
|
||||
file: TFile,
|
||||
preferredHeading: string | null
|
||||
preferredHeading: string | null,
|
||||
debugLogging?: boolean
|
||||
): Promise<{ startLine: number; endLine: number; content: string; heading: string | null } | null> {
|
||||
console.log("[selectSectionForEdge] Starting, preferredHeading:", preferredHeading);
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] Starting, preferredHeading:", preferredHeading);
|
||||
}
|
||||
const content = await app.vault.read(file);
|
||||
const sections = splitIntoSections(content);
|
||||
console.log("[selectSectionForEdge] Found", sections.length, "sections");
|
||||
console.log("[selectSectionForEdge] Section headings:", sections.map(s => s.heading));
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] Found", sections.length, "sections");
|
||||
console.log("[selectSectionForEdge] Section headings:", sections.map(s => s.heading));
|
||||
}
|
||||
|
||||
// Filter out special zones
|
||||
const contentSections = sections.filter(
|
||||
(s) => s.heading !== "Kandidaten" && s.heading !== "Note-Verbindungen"
|
||||
);
|
||||
console.log("[selectSectionForEdge] Content sections after filtering:", contentSections.length);
|
||||
console.log("[selectSectionForEdge] Content section headings:", contentSections.map(s => s.heading));
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] Content sections after filtering:", contentSections.length);
|
||||
console.log("[selectSectionForEdge] Content section headings:", contentSections.map(s => s.heading));
|
||||
}
|
||||
|
||||
if (contentSections.length === 0) {
|
||||
console.log("[selectSectionForEdge] No content sections found");
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] No content sections found");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +370,9 @@ async function selectSectionForEdge(
|
|||
if (contentSections.length === 1) {
|
||||
const section = contentSections[0];
|
||||
if (!section) return null;
|
||||
console.log("[selectSectionForEdge] Using single section:", section.heading || "(Root)");
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] Using single section:", section.heading || "(Root)");
|
||||
}
|
||||
return {
|
||||
startLine: section.startLine,
|
||||
endLine: section.endLine,
|
||||
|
|
@ -335,8 +384,10 @@ async function selectSectionForEdge(
|
|||
// Multiple sections - always show selection modal
|
||||
// If preferred heading matches, highlight it or select it by default
|
||||
// But still show the modal so user can confirm or choose differently
|
||||
console.log("[selectSectionForEdge] Multiple sections found, showing selection modal");
|
||||
console.log("[selectSectionForEdge] Preferred heading:", preferredHeading);
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] Multiple sections found, showing selection modal");
|
||||
console.log("[selectSectionForEdge] Preferred heading:", preferredHeading);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
const modal = new Modal(app);
|
||||
|
|
@ -356,7 +407,7 @@ async function selectSectionForEdge(
|
|||
for (const section of contentSections) {
|
||||
if (!section) continue;
|
||||
const sectionName = section.heading || "(Root section)";
|
||||
const isPreferred = preferredHeading && section.heading === preferredHeading;
|
||||
const isPreferred = preferredHeading && headingsMatch(section.heading, preferredHeading);
|
||||
const setting = new Setting(modal.contentEl);
|
||||
|
||||
if (isPreferred) {
|
||||
|
|
@ -375,7 +426,9 @@ async function selectSectionForEdge(
|
|||
btn.setButtonText("Select");
|
||||
}
|
||||
btn.onClick(() => {
|
||||
console.log("[selectSectionForEdge] User selected section:", sectionName);
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] User selected section:", sectionName);
|
||||
}
|
||||
doResolve({
|
||||
startLine: section.startLine,
|
||||
endLine: section.endLine,
|
||||
|
|
@ -388,9 +441,13 @@ async function selectSectionForEdge(
|
|||
}
|
||||
|
||||
modal.onClose = () => {
|
||||
console.log("[selectSectionForEdge] Modal closed, resolved:", resolved);
|
||||
if (debugLogging) {
|
||||
console.log("[selectSectionForEdge] Modal closed, resolved:", resolved);
|
||||
if (!resolved) {
|
||||
console.log("[selectSectionForEdge] Resolving with null (user cancelled)");
|
||||
}
|
||||
}
|
||||
if (!resolved) {
|
||||
console.log("[selectSectionForEdge] Resolving with null (user cancelled)");
|
||||
doResolve(null);
|
||||
}
|
||||
};
|
||||
|
|
@ -413,127 +470,21 @@ async function insertEdgeInSection(
|
|||
const content = editor.getValue();
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
// Get wrapper settings
|
||||
const sectionContent = lines.slice(section.startLine, section.endLine).join("\n");
|
||||
const wrapperCalloutType = settings.mappingWrapperCalloutType || "abstract";
|
||||
const wrapperTitle = settings.mappingWrapperTitle || "🕸️ Semantic Mapping";
|
||||
const wrapperFolded = settings.mappingWrapperFolded !== false;
|
||||
|
||||
// Extract existing mappings from section
|
||||
const mappingState = extractExistingMappings(section.content, wrapperCalloutType, wrapperTitle);
|
||||
const newSectionContent = computeSectionContentAfterInsertEdge(sectionContent, edgeType, targetLink, {
|
||||
wrapperCalloutType,
|
||||
wrapperTitle,
|
||||
wrapperFolded,
|
||||
});
|
||||
|
||||
// Check if section has semantic mapping block
|
||||
const hasMappingBlock = mappingState.wrapperBlockStartLine !== null;
|
||||
|
||||
if (hasMappingBlock && mappingState.wrapperBlockStartLine !== null && mappingState.wrapperBlockEndLine !== null) {
|
||||
console.log("[insertEdgeInSection] Inserting into existing mapping block");
|
||||
// Insert edge into existing mapping block
|
||||
const sectionLines = section.content.split(/\r?\n/);
|
||||
const wrapperStart = mappingState.wrapperBlockStartLine;
|
||||
const wrapperEnd = mappingState.wrapperBlockEndLine;
|
||||
|
||||
// Find if edgeType group already exists in wrapper block
|
||||
const wrapperBlockContent = sectionLines.slice(wrapperStart, wrapperEnd).join("\n");
|
||||
console.log("[insertEdgeInSection] Wrapper block content length:", wrapperBlockContent.length);
|
||||
|
||||
const edgeTypeGroupRegex = new RegExp(`(>>\\s*\\[!edge\\]\\s+${edgeType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?)(?=\\n\\n|\\n>>\\s*\\[!edge\\]|\\n>\\s*\\^map-|$)`);
|
||||
const edgeTypeMatch = wrapperBlockContent.match(edgeTypeGroupRegex);
|
||||
console.log("[insertEdgeInSection] Edge type group match:", edgeTypeMatch ? "found" : "not found");
|
||||
|
||||
if (edgeTypeMatch && edgeTypeMatch[1]) {
|
||||
console.log("[insertEdgeInSection] Appending to existing edge type group");
|
||||
// Edge type group exists - append to it
|
||||
const newEdge = `>> [[${targetLink}]]\n`;
|
||||
const updatedGroup = edgeTypeMatch[1].trimEnd() + "\n" + newEdge;
|
||||
const updatedWrapperBlock = wrapperBlockContent.replace(edgeTypeMatch[0], updatedGroup);
|
||||
|
||||
// Replace wrapper block in section
|
||||
const beforeWrapper = sectionLines.slice(0, wrapperStart).join("\n");
|
||||
const afterWrapper = sectionLines.slice(wrapperEnd).join("\n");
|
||||
const newSectionContent = beforeWrapper + "\n" + updatedWrapperBlock + "\n" + afterWrapper;
|
||||
|
||||
// Replace section in full content
|
||||
const beforeSection = lines.slice(0, section.startLine).join("\n");
|
||||
const afterSection = lines.slice(section.endLine).join("\n");
|
||||
const newContent = beforeSection + "\n" + newSectionContent + "\n" + afterSection;
|
||||
console.log("[insertEdgeInSection] Setting new content, length:", newContent.length);
|
||||
editor.setValue(newContent);
|
||||
console.log("[insertEdgeInSection] Content updated successfully");
|
||||
return;
|
||||
} else {
|
||||
console.log("[insertEdgeInSection] Adding new edge type group");
|
||||
// Edge type group doesn't exist - add new group before block-id marker or at end
|
||||
const blockIdMatch = wrapperBlockContent.match(/\n>?\s*\^map-/);
|
||||
const insertPos = blockIdMatch ? blockIdMatch.index || wrapperBlockContent.length : wrapperBlockContent.length;
|
||||
console.log("[insertEdgeInSection] Insert position:", insertPos);
|
||||
|
||||
const newEdgeGroup = `\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
|
||||
const updatedWrapperBlock =
|
||||
wrapperBlockContent.slice(0, insertPos) +
|
||||
newEdgeGroup +
|
||||
wrapperBlockContent.slice(insertPos);
|
||||
|
||||
// Replace wrapper block in section
|
||||
const beforeWrapper = sectionLines.slice(0, wrapperStart).join("\n");
|
||||
const afterWrapper = sectionLines.slice(wrapperEnd).join("\n");
|
||||
const newSectionContent = beforeWrapper + "\n" + updatedWrapperBlock + "\n" + afterWrapper;
|
||||
|
||||
// Replace section in full content
|
||||
const beforeSection = lines.slice(0, section.startLine).join("\n");
|
||||
const afterSection = lines.slice(section.endLine).join("\n");
|
||||
const newContent = beforeSection + "\n" + newSectionContent + "\n" + afterSection;
|
||||
console.log("[insertEdgeInSection] Setting new content with new edge group, length:", newContent.length);
|
||||
editor.setValue(newContent);
|
||||
console.log("[insertEdgeInSection] Content updated successfully");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new mapping block at end of section
|
||||
console.log("[insertEdgeInSection] Creating new mapping block");
|
||||
const existingMappings = new Map<string, string>();
|
||||
const targetLinkBasename = targetLink.split("#")[0]?.split("|")[0]?.trim() || targetLink;
|
||||
existingMappings.set(targetLinkBasename, edgeType);
|
||||
console.log("[insertEdgeInSection] Target link basename:", targetLinkBasename);
|
||||
|
||||
const newMappingBlock = buildMappingBlock(
|
||||
[targetLinkBasename],
|
||||
existingMappings,
|
||||
{
|
||||
wrapperCalloutType,
|
||||
wrapperTitle,
|
||||
wrapperFolded,
|
||||
defaultEdgeType: settings.defaultEdgeType || "",
|
||||
assignUnmapped: "none",
|
||||
}
|
||||
);
|
||||
console.log("[insertEdgeInSection] New mapping block:", newMappingBlock ? "created" : "null");
|
||||
|
||||
if (!newMappingBlock) {
|
||||
console.log("[insertEdgeInSection] Using fallback mapping block");
|
||||
// Fallback
|
||||
const foldMarker = wrapperFolded ? "-" : "+";
|
||||
const mappingBlock = `\n\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
|
||||
const newSectionContent = section.content.trimEnd() + mappingBlock;
|
||||
|
||||
const beforeSection = lines.slice(0, section.startLine).join("\n");
|
||||
const afterSection = lines.slice(section.endLine).join("\n");
|
||||
const newContent = beforeSection + "\n" + newSectionContent + "\n" + afterSection;
|
||||
console.log("[insertEdgeInSection] Setting fallback content, length:", newContent.length);
|
||||
editor.setValue(newContent);
|
||||
console.log("[insertEdgeInSection] Fallback content updated");
|
||||
return;
|
||||
}
|
||||
|
||||
const newSectionContent = insertMappingBlock(section.content, newMappingBlock);
|
||||
console.log("[insertEdgeInSection] New section content length:", newSectionContent.length);
|
||||
|
||||
// Replace section in content
|
||||
const beforeSection = lines.slice(0, section.startLine).join("\n");
|
||||
const afterSection = lines.slice(section.endLine).join("\n");
|
||||
const newContent = beforeSection + "\n" + newSectionContent + "\n" + afterSection;
|
||||
console.log("[insertEdgeInSection] Final content length:", newContent.length);
|
||||
editor.setValue(newContent);
|
||||
console.log("[insertEdgeInSection] Content updated successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -600,7 +551,8 @@ async function filterSuggestedEdgeTypes(
|
|||
sourceType: string | null,
|
||||
targetType: string | null,
|
||||
graphSchema: any,
|
||||
direction: "forward" | "backward" = "forward"
|
||||
direction: "forward" | "backward" = "forward",
|
||||
debugLogging?: boolean
|
||||
): Promise<string[]> {
|
||||
// Step 1: Filter by direction
|
||||
// For forward: keep only types that are NOT inverses of other suggested types
|
||||
|
|
@ -696,6 +648,14 @@ async function filterSuggestedEdgeTypes(
|
|||
graphSchema
|
||||
);
|
||||
|
||||
if (debugLogging) {
|
||||
console.log(`[filterSuggestedEdgeTypes] Schema filtering for ${sourceType}→${targetType}:`, {
|
||||
typical: schemaSuggestions.typical,
|
||||
prohibited: schemaSuggestions.prohibited,
|
||||
beforeFilter: filteredByDirection.length
|
||||
});
|
||||
}
|
||||
|
||||
// Keep only suggested types that are either:
|
||||
// - In typical (recommended for this source->target)
|
||||
// - Not prohibited
|
||||
|
|
@ -710,15 +670,23 @@ async function filterSuggestedEdgeTypes(
|
|||
if (isTypical || (!isProhibited && filteredByDirection.length <= 3)) {
|
||||
// Keep if typical, or if not prohibited and we don't have many options
|
||||
finalFiltered.push(type);
|
||||
} else if (debugLogging) {
|
||||
console.log(`[filterSuggestedEdgeTypes] Filtered out ${type}: isTypical=${isTypical}, isProhibited=${isProhibited}, hasManyOptions=${filteredByDirection.length > 3}`);
|
||||
}
|
||||
} else {
|
||||
// No schema recommendations: keep all (unless prohibited)
|
||||
if (!isProhibited) {
|
||||
finalFiltered.push(type);
|
||||
} else if (debugLogging) {
|
||||
console.log(`[filterSuggestedEdgeTypes] Filtered out ${type}: prohibited`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugLogging) {
|
||||
console.log(`[filterSuggestedEdgeTypes] After schema filter: ${finalFiltered.length} types`, finalFiltered);
|
||||
}
|
||||
|
||||
// If we filtered everything out, fall back to direction-filtered list
|
||||
return finalFiltered.length > 0 ? finalFiltered : filteredByDirection;
|
||||
}
|
||||
|
|
@ -737,11 +705,14 @@ async function chooseEdgeType(
|
|||
sourceType: string | null = null,
|
||||
targetType: string | null = null,
|
||||
graphSchema: any = null,
|
||||
direction: "forward" | "backward" = "forward"
|
||||
direction: "forward" | "backward" = "forward",
|
||||
debugLogging?: boolean
|
||||
): Promise<string | null> {
|
||||
console.log("[chooseEdgeType] Starting, suggestedTypes:", suggestedTypes);
|
||||
console.log("[chooseEdgeType] sourceType:", sourceType, "targetType:", targetType, "direction:", direction);
|
||||
console.log("[chooseEdgeType] edgeVocabulary entries:", edgeVocabulary.byCanonical.size);
|
||||
if (debugLogging) {
|
||||
console.log("[chooseEdgeType] Starting, suggestedTypes:", suggestedTypes);
|
||||
console.log("[chooseEdgeType] sourceType:", sourceType, "targetType:", targetType, "direction:", direction);
|
||||
console.log("[chooseEdgeType] edgeVocabulary entries:", edgeVocabulary.byCanonical.size);
|
||||
}
|
||||
|
||||
// Filter suggested types by direction and note types
|
||||
const filteredSuggestedTypes = await filterSuggestedEdgeTypes(
|
||||
|
|
@ -750,9 +721,18 @@ async function chooseEdgeType(
|
|||
sourceType,
|
||||
targetType,
|
||||
graphSchema,
|
||||
direction
|
||||
direction,
|
||||
debugLogging
|
||||
);
|
||||
console.log("[chooseEdgeType] Filtered suggested types:", filteredSuggestedTypes);
|
||||
if (debugLogging) {
|
||||
console.log("[chooseEdgeType] Filtered suggested types:", filteredSuggestedTypes);
|
||||
console.log("[chooseEdgeType] Filtering details:", {
|
||||
inputCount: suggestedTypes.length,
|
||||
outputCount: filteredSuggestedTypes.length,
|
||||
removed: suggestedTypes.filter(t => !filteredSuggestedTypes.includes(t)),
|
||||
graphSchemaAvailable: !!graphSchema
|
||||
});
|
||||
}
|
||||
|
||||
// Show edge type chooser with filtered suggested types
|
||||
const modal = new EdgeTypeChooserModal(
|
||||
|
|
@ -764,17 +744,25 @@ async function chooseEdgeType(
|
|||
filteredSuggestedTypes // Pass filtered suggested types to modal
|
||||
);
|
||||
|
||||
console.log("[chooseEdgeType] Modal created, calling show()...");
|
||||
if (debugLogging) {
|
||||
console.log("[chooseEdgeType] Modal created, calling show()...");
|
||||
}
|
||||
const result = await modal.show();
|
||||
console.log("[chooseEdgeType] Modal result:", result);
|
||||
if (debugLogging) {
|
||||
console.log("[chooseEdgeType] Modal result:", result);
|
||||
}
|
||||
|
||||
// Return alias if selected, otherwise canonical
|
||||
if (!result) {
|
||||
console.log("[chooseEdgeType] No result, returning null");
|
||||
if (debugLogging) {
|
||||
console.log("[chooseEdgeType] No result, returning null");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedType = result.alias || result.edgeType;
|
||||
console.log("[chooseEdgeType] Returning selected type:", selectedType);
|
||||
if (debugLogging) {
|
||||
console.log("[chooseEdgeType] Returning selected type:", selectedType);
|
||||
}
|
||||
return selectedType;
|
||||
}
|
||||
|
|
|
|||
451
styles.css
451
styles.css
|
|
@ -287,3 +287,454 @@ If your plugin does not need CSS, delete this file.
|
|||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
}
|
||||
|
||||
/* Chain Workbench Modal - Wide two-column layout */
|
||||
.chain-workbench-modal.modal {
|
||||
width: clamp(1200px, 95vw, 1600px) !important;
|
||||
height: clamp(700px, 90vh, 1000px) !important;
|
||||
max-width: 95vw !important;
|
||||
max-height: 95vh !important;
|
||||
}
|
||||
|
||||
.chain-workbench-modal .modal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chain-workbench-modal .modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
/* Workbench Header */
|
||||
.workbench-header {
|
||||
flex-shrink: 0;
|
||||
padding-bottom: 1em;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.workbench-header h2 {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.workbench-header .context-info {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Workbench Filters */
|
||||
.workbench-filters {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1em;
|
||||
padding: 0.5em;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.workbench-filters label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workbench-filters select,
|
||||
.workbench-filters input {
|
||||
padding: 0.25em 0.5em;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
/* Workbench Main Container - Two Column Layout */
|
||||
.workbench-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 1.5em;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Left Column: Tree View */
|
||||
.workbench-tree {
|
||||
flex: 0 0 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
padding-right: 1em;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.workbench-tree h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Template Tree */
|
||||
.template-tree {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.template-tree-item {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.template-tree-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.template-tree-header:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.template-tree-header.expanded {
|
||||
background-color: var(--background-modifier-active);
|
||||
}
|
||||
|
||||
.template-tree-toggle {
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.template-tree-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.template-tree-count {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.template-tree-chains {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 0.25em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.template-tree-item.expanded .template-tree-chains {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chain-item {
|
||||
padding: 0.5em;
|
||||
margin: 0.25em 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.chain-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.chain-item.selected {
|
||||
background-color: var(--interactive-hover);
|
||||
border-color: var(--interactive-accent);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.chain-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.chain-status-icon {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chain-item-info {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.chain-item-notes {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Right Column: Details View */
|
||||
.workbench-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workbench-details h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.workbench-details-placeholder {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
/* Chain Visualization */
|
||||
.chain-visualization {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.chain-visualization-header {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.chain-visualization-header h4 {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.chain-visualization-header .chain-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Chain Flow Table */
|
||||
.chain-flow-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.chain-flow-table th,
|
||||
.chain-flow-table td {
|
||||
padding: 0.75em;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.chain-flow-table th {
|
||||
background-color: var(--background-secondary);
|
||||
font-weight: 600;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chain-flow-table tr:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.chain-flow-table .slot-cell {
|
||||
font-weight: 500;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.chain-flow-table .edge-cell {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chain-flow-table .node-cell {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chain-flow-table .missing {
|
||||
color: var(--text-error);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Mermaid Graph Container */
|
||||
.chain-mermaid-container {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.chain-mermaid-container .mermaid {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Edge Detection Details */
|
||||
.edge-detection-details {
|
||||
margin-top: 2em;
|
||||
padding-top: 2em;
|
||||
border-top: 2px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.edge-detection-details h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.edge-detection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.edge-detection-item {
|
||||
padding: 1em;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.edge-detection-item.matched {
|
||||
border-left-color: var(--interactive-success);
|
||||
}
|
||||
|
||||
.edge-detection-item.not-matched {
|
||||
border-left-color: var(--text-error);
|
||||
}
|
||||
|
||||
.edge-detection-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edge-detection-status {
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.edge-detection-status.matched {
|
||||
background-color: var(--interactive-success);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.edge-detection-status.not-matched {
|
||||
background-color: var(--text-error);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.edge-detection-info {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.edge-detection-info .info-row {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.edge-detection-info .info-label {
|
||||
font-weight: 500;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Todos Section */
|
||||
.workbench-todos-section {
|
||||
margin-top: 2em;
|
||||
padding-top: 2em;
|
||||
border-top: 2px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workbench-todos-section h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.todos-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
padding: 1em;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.todo-item.high {
|
||||
border-left-color: var(--text-error);
|
||||
}
|
||||
|
||||
.todo-item.medium {
|
||||
border-left-color: var(--text-warning);
|
||||
}
|
||||
|
||||
.todo-item.low {
|
||||
border-left-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.todo-description {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.todo-priority {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.todo-actions {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.todo-actions button {
|
||||
padding: 0.4em 0.8em;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background: var(--background-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.todo-actions button:hover {
|
||||
background: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.todo-actions button:active {
|
||||
background: var(--interactive-active);
|
||||
}
|
||||
|
||||
/* Node type badge */
|
||||
.node-type-badge {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
/* Info text */
|
||||
.info-text {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
|||
19
tests/fixtures/Tests/06_section_type_override.md
vendored
Normal file
19
tests/fixtures/Tests/06_section_type_override.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
id: t_override_001
|
||||
title: section_type_override
|
||||
type: concept
|
||||
status: draft
|
||||
---
|
||||
|
||||
## Kontext
|
||||
|
||||
> [!section] experience
|
||||
|
||||
Diese Note hat type: concept im Frontmatter, aber diese Section hat explizit [!section] experience.
|
||||
Für Chain-Matching soll effectiveType = "experience" verwendet werden (Section-Type überschreibt Note-Type).
|
||||
|
||||
> [!edge] ausgelöst_durch
|
||||
> [[02_event_trigger_detail#Detail]]
|
||||
|
||||
> [!edge] wirkt_auf
|
||||
> [[03_insight_transformation#Kern]]
|
||||
23
tests/fixtures/Tests/intra_note_block_ref.md
vendored
Normal file
23
tests/fixtures/Tests/intra_note_block_ref.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
type: experience
|
||||
---
|
||||
|
||||
## Situation (Was ist passiert?) ^situation
|
||||
|
||||
Content.
|
||||
|
||||
## Ergebnis & Auswirkung ^impact
|
||||
|
||||
> [!edge] beherrscht_von
|
||||
> [[#^situation]]
|
||||
|
||||
> [!edge] impacts
|
||||
> [[#^next]]
|
||||
|
||||
## Nächster Schritt ^next
|
||||
|
||||
Content.
|
||||
|
||||
## Reflexion & Learning ^learning
|
||||
|
||||
Content.
|
||||
Loading…
Reference in New Issue
Block a user