Add graph schema loading and validation functionality
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- Introduced methods for loading and reloading graph schema, including debounced reload on file changes.
- Enhanced the semantic mapping builder to utilize the new graph schema loading mechanism for improved caching and live updates.
- Added a validation button in the settings interface to check the existence and validity of the graph schema file, providing user feedback on the schema status.
- Improved error handling for graph schema loading to ensure clear notifications for users in case of issues.
This commit is contained in:
Lars 2026-01-17 07:42:17 +01:00
parent 58b6ffffed
commit 1f9211cbc5
5 changed files with 164 additions and 8 deletions

View File

@ -20,6 +20,8 @@ import { InterviewWizardModal, type WizardResult } from "./ui/InterviewWizardMod
import { extractTargetFromAnchor } from "./interview/extractTargetFromAnchor";
import { buildSemanticMappings } from "./mapping/semanticMappingBuilder";
import { ConfirmOverwriteModal } from "./ui/ConfirmOverwriteModal";
import { GraphSchemaLoader } from "./schema/GraphSchemaLoader";
import type { GraphSchema } from "./mapping/graphSchema";
export default class MindnetCausalAssistantPlugin extends Plugin {
settings: MindnetSettings;
@ -27,6 +29,8 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
private reloadDebounceTimer: number | null = null;
private interviewConfig: InterviewConfig | null = null;
private interviewConfigReloadDebounceTimer: number | null = null;
private graphSchema: GraphSchema | null = null;
private graphSchemaReloadDebounceTimer: number | null = null;
async onload(): Promise<void> {
await this.loadSettings();
@ -127,6 +131,22 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
this.interviewConfigReloadDebounceTimer = null;
}, 200);
}
// Check if modified file matches graph schema path
const normalizedGraphSchemaPath = normalizeVaultPath(this.settings.graphSchemaPath);
if (normalizedFilePath === normalizedGraphSchemaPath ||
normalizedFilePath === `/${normalizedGraphSchemaPath}` ||
normalizedFilePath.endsWith(`/${normalizedGraphSchemaPath}`)) {
// Debounce reload
if (this.graphSchemaReloadDebounceTimer !== null) {
window.clearTimeout(this.graphSchemaReloadDebounceTimer);
}
this.graphSchemaReloadDebounceTimer = window.setTimeout(async () => {
await this.reloadGraphSchema();
this.graphSchemaReloadDebounceTimer = null;
}, 200);
}
})
);
@ -384,7 +404,8 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
this.app,
activeFile,
this.settings,
allowOverwrite
allowOverwrite,
this
);
// Show summary
@ -667,4 +688,58 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
console.error(e);
}
}
/**
* Ensure graph schema is loaded. Auto-loads if not present.
* Returns GraphSchema instance or null on failure.
*/
public async ensureGraphSchemaLoaded(): Promise<GraphSchema | null> {
if (this.graphSchema) {
return this.graphSchema;
}
try {
this.graphSchema = await GraphSchemaLoader.load(
this.app,
this.settings.graphSchemaPath
);
const ruleCount = this.graphSchema.schema.size;
console.log(`Graph schema auto-loaded: ${ruleCount} source types`);
return this.graphSchema;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("not found") || msg.includes("file not found")) {
console.warn("Graph schema not found. Check the path in plugin settings.");
} else {
console.error(`Failed to load graph schema: ${msg}`);
}
return null;
}
}
/**
* Reload graph schema from file. Used by manual command and live reload.
*/
public async reloadGraphSchema(): Promise<void> {
try {
this.graphSchema = await GraphSchemaLoader.load(
this.app,
this.settings.graphSchemaPath
);
const ruleCount = this.graphSchema.schema.size;
console.log(`Graph schema reloaded: ${ruleCount} source types`);
new Notice(`Graph schema reloaded: ${ruleCount} rules`);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("not found") || msg.includes("file not found")) {
new Notice("Graph schema not found. Configure path in plugin settings.");
} else {
new Notice(`Failed to reload graph schema: ${msg}`);
}
console.error(e);
this.graphSchema = null; // Clear cache on error
}
}
}

View File

@ -39,7 +39,8 @@ export async function buildSemanticMappings(
app: App,
file: TFile,
settings: MindnetSettings,
allowOverwrite: boolean
allowOverwrite: boolean,
plugin?: { ensureGraphSchemaLoaded?: () => Promise<GraphSchema | null> }
): Promise<BuildResult> {
const content = await app.vault.read(file);
const lines = content.split(/\r?\n/);
@ -64,12 +65,18 @@ export async function buildSemanticMappings(
}
// Load graph schema (optional, continue if not found)
try {
const schemaText = await VocabularyLoader.loadText(app, settings.graphSchemaPath);
graphSchema = parseGraphSchema(schemaText);
} catch (e) {
console.warn(`Graph schema not available: ${e instanceof Error ? e.message : String(e)}`);
// Continue without schema
if (plugin && plugin.ensureGraphSchemaLoaded) {
// Use plugin's ensureGraphSchemaLoaded for caching and live reload
graphSchema = await plugin.ensureGraphSchemaLoaded();
} else {
// Fallback: direct load (no caching)
try {
const schemaText = await VocabularyLoader.loadText(app, settings.graphSchemaPath);
graphSchema = parseGraphSchema(schemaText);
} catch (e) {
console.warn(`Graph schema not available: ${e instanceof Error ? e.message : String(e)}`);
// Continue without schema
}
}
}

View File

@ -0,0 +1,30 @@
/**
* Graph Schema loader with caching.
*/
import { App, Notice } from "obsidian";
import { normalizeVaultPath } from "../settings";
import { parseGraphSchema, type GraphSchema } from "../mapping/graphSchema";
import { VocabularyLoader } from "../vocab/VocabularyLoader";
export class GraphSchemaLoader {
/**
* Load graph schema from vault.
*/
static async load(app: App, vaultRelativePath: string): Promise<GraphSchema> {
const text = await VocabularyLoader.loadText(app, vaultRelativePath);
return parseGraphSchema(text);
}
/**
* Validate that graph schema file exists in vault.
*/
static async validate(app: App, vaultRelativePath: string): Promise<boolean> {
try {
await VocabularyLoader.loadText(app, vaultRelativePath);
return true;
} catch (e) {
return false;
}
}
}

View File

@ -0,0 +1,15 @@
/**
* Unit tests for GraphSchemaLoader.
*/
import { describe, it, expect } from "vitest";
import { GraphSchemaLoader } from "../../schema/GraphSchemaLoader";
import { normalizeVaultPath } from "../../settings";
describe("GraphSchemaLoader", () => {
it("should normalize vault paths correctly", () => {
expect(normalizeVaultPath("_system/dictionary/graph_schema.md")).toBe("_system/dictionary/graph_schema.md");
expect(normalizeVaultPath("_system\\dictionary\\graph_schema.md")).toBe("_system/dictionary/graph_schema.md");
expect(normalizeVaultPath(" _system/dictionary/graph_schema.md ")).toBe("_system/dictionary/graph_schema.md");
});
});

View File

@ -64,6 +64,35 @@ export class MindnetSettingTab extends PluginSettingTab {
this.plugin.settings.graphSchemaPath = value;
await this.plugin.saveSettings();
})
)
.addButton((button) =>
button
.setButtonText("Validate")
.setCta()
.onClick(async () => {
try {
const { GraphSchemaLoader } = await import("../schema/GraphSchemaLoader");
const isValid = await GraphSchemaLoader.validate(
this.app,
this.plugin.settings.graphSchemaPath
);
if (isValid) {
const schema = await GraphSchemaLoader.load(
this.app,
this.plugin.settings.graphSchemaPath
);
const ruleCount = schema.schema.size;
new Notice(
`Graph schema file found (${ruleCount} source types)`
);
} else {
new Notice("Graph schema file not found or invalid");
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
new Notice(`Failed to validate graph schema: ${msg}`);
}
})
);
// Strict mode toggle