import { Notice, Plugin, TFile } from "obsidian"; import { DEFAULT_SETTINGS, type MindnetSettings, normalizeVaultPath } from "./settings"; import { VocabularyLoader } from "./vocab/VocabularyLoader"; import { parseEdgeVocabulary } from "./vocab/parseEdgeVocabulary"; import { Vocabulary } from "./vocab/Vocabulary"; import { LintEngine } from "./lint/LintEngine"; import { MindnetSettingTab } from "./ui/MindnetSettingTab"; import { exportGraph } from "./export/exportGraph"; import { buildGraph } from "./graph/GraphBuilder"; import { buildIndex } from "./graph/GraphIndex"; import { traverseForward, traverseBackward, type Path } from "./graph/traverse"; import { renderChainReport } from "./graph/renderChainReport"; import { extractFrontmatterId } from "./parser/parseFrontmatter"; export default class MindnetCausalAssistantPlugin extends Plugin { settings: MindnetSettings; private vocabulary: Vocabulary | null = null; private reloadDebounceTimer: number | null = null; async onload(): Promise { await this.loadSettings(); // Add settings tab this.addSettingTab(new MindnetSettingTab(this.app, this)); // Register live reload for edge vocabulary file this.registerEvent( this.app.vault.on("modify", async (file: TFile) => { const normalizedFilePath = normalizeVaultPath(file.path); const normalizedVocabPath = normalizeVaultPath(this.settings.edgeVocabularyPath); // Check if modified file matches vocabulary path (exact match or ends with) if (normalizedFilePath === normalizedVocabPath || normalizedFilePath === `/${normalizedVocabPath}` || normalizedFilePath.endsWith(`/${normalizedVocabPath}`)) { // Debounce reload to avoid multiple rapid reloads if (this.reloadDebounceTimer !== null) { window.clearTimeout(this.reloadDebounceTimer); } this.reloadDebounceTimer = window.setTimeout(async () => { await this.reloadVocabulary(); this.reloadDebounceTimer = null; }, 200); } }) ); this.addCommand({ id: "mindnet-reload-edge-vocabulary", name: "Mindnet: Reload edge vocabulary", callback: async () => { await this.reloadVocabulary(); }, }); this.addCommand({ id: "mindnet-validate-current-note", name: "Mindnet: Validate current note", callback: async () => { try { const vocabulary = await this.ensureVocabularyLoaded(); if (!vocabulary) { return; } const findings = await LintEngine.lintCurrentNote( this.app, vocabulary, { showCanonicalHints: this.settings.showCanonicalHints } ); // Count findings by severity const errorCount = findings.filter(f => f.severity === "ERROR").length; const warnCount = findings.filter(f => f.severity === "WARN").length; const infoCount = findings.filter(f => f.severity === "INFO").length; // Show summary notice new Notice(`Lint: ${errorCount} errors, ${warnCount} warnings, ${infoCount} info`); // Log findings to console console.log("=== Lint Findings ==="); for (const finding of findings) { console.log(`[${finding.severity}] ${finding.ruleId}: ${finding.message} (${finding.filePath}:${finding.lineStart})`); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to validate note: ${msg}`); console.error(e); } }, }); this.addCommand({ id: "mindnet-export-graph", name: "Mindnet: Export graph", callback: async () => { try { const vocabulary = await this.ensureVocabularyLoaded(); if (!vocabulary) { return; } const outputPath = "_system/exports/graph_export.json"; await exportGraph(this.app, vocabulary, outputPath); new Notice(`Graph exported to ${outputPath}`); console.log(`Graph exported: ${outputPath}`); } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to export graph: ${msg}`); console.error(e); } }, }); this.addCommand({ id: "mindnet-show-chains-from-current-note", name: "Mindnet: Show chains from current note", callback: async () => { try { const vocabulary = await this.ensureVocabularyLoaded(); if (!vocabulary) { return; } const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) { new Notice("No active file"); return; } if (activeFile.extension !== "md") { new Notice("Active file is not a markdown file"); return; } // Extract start ID from frontmatter const content = await this.app.vault.read(activeFile); const startId = extractFrontmatterId(content); if (!startId) { new Notice("Current note has no frontmatter ID. Add 'id: ' to frontmatter."); return; } // Build graph const graph = await buildGraph(this.app, vocabulary); // Get start node meta const startMeta = graph.idToMeta.get(startId); if (!startMeta) { new Notice(`Start node ID '${startId}' not found in graph`); return; } // Build index const index = buildIndex(graph.edges); // Run traversal let allPaths: Path[] = []; if (this.settings.chainDirection === "forward" || this.settings.chainDirection === "both") { const forwardPaths = traverseForward( index, startId, this.settings.maxHops, 200 ); allPaths = [...allPaths, ...forwardPaths]; } if (this.settings.chainDirection === "backward" || this.settings.chainDirection === "both") { const backwardPaths = traverseBackward( index, startId, this.settings.maxHops, 200 ); allPaths = [...allPaths, ...backwardPaths]; } // Render report const report = renderChainReport({ startId, startMeta, paths: allPaths, direction: this.settings.chainDirection, maxHops: this.settings.maxHops, maxPaths: 200, warnings: graph.warnings, idToMeta: graph.idToMeta, }); // Write report file const reportPath = "_system/exports/chain_report.md"; await this.app.vault.adapter.write(reportPath, report); // Open report const reportFile = this.app.vault.getAbstractFileByPath(reportPath); if (reportFile && reportFile instanceof TFile) { await this.app.workspace.openLinkText(reportPath, "", true); } // Show summary const uniqueNodes = new Set(); for (const path of allPaths) { for (const nodeId of path.nodes) { uniqueNodes.add(nodeId); } } const totalWarnings = graph.warnings.missingFrontmatterId.length + graph.warnings.missingTargetFile.length + graph.warnings.missingTargetId.length; new Notice( `Chains: ${allPaths.length} paths, ${uniqueNodes.size} nodes, ${totalWarnings} warnings` ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to generate chain report: ${msg}`); console.error(e); } }, }); } onunload(): void { // nothing yet } private async loadSettings(): Promise { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } async saveSettings(): Promise { await this.saveData(this.settings); } /** * Ensure vocabulary is loaded. Auto-loads if not present. * Returns Vocabulary instance or null on failure. */ private async ensureVocabularyLoaded(): Promise { if (this.vocabulary) { return this.vocabulary; } try { const text = await VocabularyLoader.loadText( this.app, this.settings.edgeVocabularyPath ); const parsed = parseEdgeVocabulary(text); this.vocabulary = new Vocabulary(parsed); const stats = this.vocabulary.getStats(); console.log("Vocabulary auto-loaded", stats); return this.vocabulary; } catch (e) { const msg = e instanceof Error ? e.message : String(e); if (msg.includes("not found") || msg.includes("Vocabulary file not found")) { new Notice("edge_vocabulary.md not found. Check the path in plugin settings."); } else { new Notice(`Failed to load vocabulary: ${msg}. Check plugin settings.`); } console.error("Failed to load vocabulary:", e); return null; } } /** * Reload vocabulary from file. Used by manual command and live reload. */ private async reloadVocabulary(): Promise { try { const text = await VocabularyLoader.loadText( this.app, this.settings.edgeVocabularyPath ); const parsed = parseEdgeVocabulary(text); this.vocabulary = new Vocabulary(parsed); const stats = this.vocabulary.getStats(); console.log("Vocabulary loaded", stats); new Notice(`Edge vocabulary reloaded: ${stats.canonicalCount} canonical, ${stats.aliasCount} aliases`); // Log normalization examples if (stats.canonicalCount > 0) { const firstCanonical = Array.from(parsed.byCanonical.keys())[0]; if (firstCanonical) { const canonicalNorm = this.vocabulary.normalize(firstCanonical); console.log(`Normalization example (canonical): "${firstCanonical}" -> canonical: ${canonicalNorm.canonical}, inverse: ${canonicalNorm.inverse}`); } if (stats.aliasCount > 0) { const firstAlias = Array.from(parsed.aliasToCanonical.keys())[0]; if (firstAlias) { const aliasNorm = this.vocabulary.normalize(firstAlias); console.log(`Normalization example (alias): "${firstAlias}" -> canonical: ${aliasNorm.canonical}, inverse: ${aliasNorm.inverse}`); } } } } catch (e) { const msg = e instanceof Error ? e.message : String(e); if (msg.includes("not found") || msg.includes("Vocabulary file not found")) { new Notice("edge_vocabulary.md not found. Configure path in plugin settings."); } else { new Notice(`Failed to reload vocabulary: ${msg}`); } console.error(e); } } }