mindnet_obsidian/src/main.ts

317 lines
11 KiB
TypeScript
Raw Normal View History

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<void> {
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: <value>' 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<string>();
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<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings(): Promise<void> {
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<Vocabulary | null> {
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<void> {
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);
}
}
2022-04-15 20:13:31 +02:00
}