From b9ae8b49b8f89c5e009dbfe9c58ae13d798b53da Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:36:46 +0200 Subject: [PATCH 01/22] feat(ontology): follow owl:imports from ontology graph, fix discovery, add tests - Gate *.network.test.ts with NETWORK_TESTS=1 env var (vitest.config.ts) - Test fetchWithCorsFallback proxy logic (3 scenarios) in rdfManager.cors.test.ts - Fix discoverReferencedOntologies: remove hardcoded urn:vg:data override on graphName (lines 1309, 1373, 1376, 1461, 1515) so requested graph is used - Add unconditional?: boolean option to bypass autoDiscoverOntologies for explicit loadOntology calls - Fire-and-forget discovery after loadOntology (!discovered guard for cycle prevention) - Hermetic owl:imports tests using in-memory mock rdfManager + fetch stub - Network reachability test skeleton for all well-known ontology entries --- src/__tests__/fixtures/ont-imported.ttl | 6 + src/__tests__/fixtures/ont-with-imports.ttl | 7 + .../stores/ontologyStore.owlImports.test.ts | 161 ++++++++++++++++++ src/__tests__/stores/ontologyStore.test.ts | 66 ++++++- .../wellKnownOntologies.network.test.ts | 75 ++++++++ src/__tests__/utils/rdfManager.cors.test.ts | 84 +++++++++ src/stores/ontologyStore.ts | 33 ++-- vitest.config.ts | 7 +- 8 files changed, 426 insertions(+), 13 deletions(-) create mode 100644 src/__tests__/fixtures/ont-imported.ttl create mode 100644 src/__tests__/fixtures/ont-with-imports.ttl create mode 100644 src/__tests__/stores/ontologyStore.owlImports.test.ts create mode 100644 src/__tests__/stores/wellKnownOntologies.network.test.ts create mode 100644 src/__tests__/utils/rdfManager.cors.test.ts diff --git a/src/__tests__/fixtures/ont-imported.ttl b/src/__tests__/fixtures/ont-imported.ttl new file mode 100644 index 00000000..e33369d5 --- /dev/null +++ b/src/__tests__/fixtures/ont-imported.ttl @@ -0,0 +1,6 @@ +@prefix owl: . +@prefix rdfs: . + + + a owl:Ontology ; + rdfs:label "Imported Test Ontology" . diff --git a/src/__tests__/fixtures/ont-with-imports.ttl b/src/__tests__/fixtures/ont-with-imports.ttl new file mode 100644 index 00000000..230c66e2 --- /dev/null +++ b/src/__tests__/fixtures/ont-with-imports.ttl @@ -0,0 +1,7 @@ +@prefix owl: . +@prefix rdfs: . + + + a owl:Ontology ; + owl:imports ; + rdfs:label "Main Test Ontology" . diff --git a/src/__tests__/stores/ontologyStore.owlImports.test.ts b/src/__tests__/stores/ontologyStore.owlImports.test.ts new file mode 100644 index 00000000..258553ce --- /dev/null +++ b/src/__tests__/stores/ontologyStore.owlImports.test.ts @@ -0,0 +1,161 @@ +// @vitest-environment node +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { Parser as N3Parser } from "n3"; +import { useOntologyStore } from "../../stores/ontologyStore"; +import { rdfManager } from "../../utils/rdfManager"; + +const MAIN_URL = "https://test.example.org/main-ont.ttl"; +const IMPORTED_URL = "https://test.example.org/imported-ont.ttl"; + +const mainTtl = readFileSync(resolve(__dirname, "../fixtures/ont-with-imports.ttl"), "utf-8"); +const importedTtl = readFileSync(resolve(__dirname, "../fixtures/ont-imported.ttl"), "utf-8"); + +function makeTurtleResponse(body: string): Response { + return new Response(body, { + status: 200, + headers: { "content-type": "text/turtle" }, + }); +} + +/** + * Minimal in-memory RDF manager mock. Parses Turtle on loadRDFIntoGraph/loadRDFFromUrl, + * returns quads via positional fetchQuadsPage(graph, offset, limit, opts) that + * ontologyStore.fetchSerializedQuads expects. + */ +function makeInMemoryRdfManager() { + const store: Map> = new Map(); + + function addQuad(graph: string, s: string, p: string, o: string) { + let quads = store.get(graph); + if (!quads) { quads = []; store.set(graph, quads); } + quads.push({ subject: s, predicate: p, object: o, graph }); + } + + function parseTurtle(text: string, graph: string) { + const parser = new N3Parser({ format: "Turtle" }); + const quads = parser.parse(text); + for (const q of quads) { + addQuad(graph, q.subject.value, q.predicate.value, q.object.value); + } + } + + return { + // loadRDFIntoGraph: parse turtle and store quads + loadRDFIntoGraph: vi.fn().mockImplementation(async (text: string, graphName: string) => { + parseTurtle(text, graphName); + }), + + // loadRDFFromUrl: fetch url and delegate to loadRDFIntoGraph + loadRDFFromUrl: vi.fn().mockImplementation(async (url: string, graphName: string) => { + const resp = await fetch(url, { signal: new AbortController().signal, redirect: "follow", headers: {} }); + if (!resp.ok) throw new Error(`fetch failed: ${resp.status}`); + const text = await resp.text(); + parseTurtle(text, graphName); + }), + + // Positional fetchQuadsPage (matches RdfPageFetcher interface in ontologyStore) + fetchQuadsPage: vi.fn().mockImplementation( + async (graph: string, _offset: number, _limit: number, opts: any) => { + const all = store.get(graph) || []; + const predFilter = opts?.filter?.predicate; + const items = predFilter ? all.filter((q) => q.predicate === predFilter) : all; + return { items, total: items.length, limit: _limit }; + }, + ), + + emitAllSubjects: vi.fn().mockResolvedValue(undefined), + + clear: vi.fn().mockImplementation(() => { store.clear(); }), + }; +} + +describe("loadOntology — owl:imports fire-and-forget discovery", () => { + let savedRdfManager: any; + + beforeEach(() => { + savedRdfManager = useOntologyStore.getState().rdfManager; + useOntologyStore.getState().clearOntologies(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + useOntologyStore.setState({ rdfManager: savedRdfManager } as any); + }); + + it("auto-loads owl:imports after explicit loadOntology, even when autoDiscoverOntologies is false", async () => { + const mockMgr = makeInMemoryRdfManager(); + useOntologyStore.setState({ rdfManager: mockMgr as any } as any); + + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation((url: string) => { + if (url === MAIN_URL) return Promise.resolve(makeTurtleResponse(mainTtl)); + if (url === IMPORTED_URL) return Promise.resolve(makeTurtleResponse(importedTtl)); + return Promise.reject(new TypeError(`Unexpected fetch: ${url}`)); + }), + ); + + // Disable autoDiscoverOntologies — fire-and-forget must still run (unconditional:true) + const { useAppConfigStore } = await import("../../stores/appConfigStore"); + const prevConfig = useAppConfigStore.getState().config; + useAppConfigStore.setState({ + config: { ...prevConfig, autoDiscoverOntologies: false }, + } as any); + + try { + await useOntologyStore.getState().loadOntology(MAIN_URL); + + // Wait for fire-and-forget discovery + inner loadOntology to complete + await vi.waitFor( + () => { + const loaded = useOntologyStore.getState().loadedOntologies; + const urls = loaded.map((o: any) => String(o.url)); + if (!urls.some((u: string) => u.includes("imported-ont"))) { + throw new Error(`imported-ont not yet in loadedOntologies: ${JSON.stringify(urls)}`); + } + }, + { timeout: 5000, interval: 50 }, + ); + + const loaded = useOntologyStore.getState().loadedOntologies; + const urls = loaded.map((o: any) => String(o.url)); + expect(urls.some((u: string) => u.includes("main-ont"))).toBe(true); + expect(urls.some((u: string) => u.includes("imported-ont"))).toBe(true); + } finally { + useAppConfigStore.setState({ config: prevConfig } as any); + } + }, 10000); + + it("does not re-trigger discovery for inner loadOntology calls (discovered:true guard)", async () => { + const mockMgr = makeInMemoryRdfManager(); + useOntologyStore.setState({ rdfManager: mockMgr as any } as any); + + const fetchMock = vi.fn().mockImplementation((url: string) => { + if (url === MAIN_URL) return Promise.resolve(makeTurtleResponse(mainTtl)); + if (url === IMPORTED_URL) return Promise.resolve(makeTurtleResponse(importedTtl)); + return Promise.reject(new TypeError(`Unexpected fetch: ${url}`)); + }); + vi.stubGlobal("fetch", fetchMock); + + await useOntologyStore.getState().loadOntology(MAIN_URL); + + await vi.waitFor( + () => { + const loaded = useOntologyStore.getState().loadedOntologies; + const urls = loaded.map((o: any) => String(o.url)); + if (!urls.some((u: string) => u.includes("imported-ont"))) { + throw new Error("imported-ont not yet loaded"); + } + }, + { timeout: 5000, interval: 50 }, + ); + + // Each URL should be fetched exactly once — no infinite recursion + const mainFetches = fetchMock.mock.calls.filter(([url]: [string]) => url === MAIN_URL).length; + const importedFetches = fetchMock.mock.calls.filter(([url]: [string]) => url === IMPORTED_URL).length; + expect(mainFetches).toBe(1); + expect(importedFetches).toBe(1); + }, 10000); +}); diff --git a/src/__tests__/stores/ontologyStore.test.ts b/src/__tests__/stores/ontologyStore.test.ts index 90775268..073c5931 100644 --- a/src/__tests__/stores/ontologyStore.test.ts +++ b/src/__tests__/stores/ontologyStore.test.ts @@ -1,5 +1,6 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { useOntologyStore } from "../../stores/ontologyStore"; +import { rdfManager } from "../../utils/rdfManager"; import validateGraph from "../../utils/graphValidation"; import { WELL_KNOWN } from "../../utils/wellKnownOntologies"; @@ -158,3 +159,66 @@ describe("Ontology Store", () => { }); }); }); + +describe("discoverReferencedOntologies — graph parameter fix", () => { + const ONTOLOGY_GRAPH = "urn:vg:ontologies"; + const IMPORT_URL = "https://example.org/imported-ontology.ttl"; + const SUBJECT = "urn:test:myOntology"; + + const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; + const OWL_ONTOLOGY = "http://www.w3.org/2002/07/owl#Ontology"; + const OWL_IMPORTS = "http://www.w3.org/2002/07/owl#imports"; + + let savedRdfManager: any; + + beforeEach(() => { + savedRdfManager = useOntologyStore.getState().rdfManager; + useOntologyStore.getState().clearOntologies(); + }); + + afterEach(() => { + useOntologyStore.setState({ rdfManager: savedRdfManager } as any); + }); + + it("returns owl:imports candidates from the requested graph, not hardcoded data graph", async () => { + const mockFetchQuadsPage = vi.fn().mockImplementation( + async (graph: string, _offset: number, _limit: number, opts: any) => { + if (graph !== ONTOLOGY_GRAPH) { + return { items: [], total: 0, limit: _limit }; + } + const predicate = opts?.filter?.predicate; + if (predicate === RDF_TYPE) { + return { + items: [{ subject: SUBJECT, predicate: RDF_TYPE, object: OWL_ONTOLOGY, graph }], + total: 1, + limit: _limit, + }; + } + if (predicate === OWL_IMPORTS) { + return { + items: [{ subject: SUBJECT, predicate: OWL_IMPORTS, object: IMPORT_URL, graph }], + total: 1, + limit: _limit, + }; + } + return { items: [], total: 0, limit: _limit }; + }, + ); + + useOntologyStore.setState({ + rdfManager: { fetchQuadsPage: mockFetchQuadsPage } as any, + } as any); + + const store = useOntologyStore.getState(); + const result = await store.discoverReferencedOntologies!({ + graphName: ONTOLOGY_GRAPH, + load: false, + }); + + expect(result.candidates).toContain(IMPORT_URL); + // All fetchQuadsPage calls must target the ontology graph, never "urn:vg:data" + for (const call of mockFetchQuadsPage.mock.calls) { + expect(call[0]).toBe(ONTOLOGY_GRAPH); + } + }); +}); diff --git a/src/__tests__/stores/wellKnownOntologies.network.test.ts b/src/__tests__/stores/wellKnownOntologies.network.test.ts new file mode 100644 index 00000000..2ebd60f7 --- /dev/null +++ b/src/__tests__/stores/wellKnownOntologies.network.test.ts @@ -0,0 +1,75 @@ +// @vitest-environment node +import { describe, it, expect } from "vitest"; +import { Readable } from "node:stream"; +import { Parser as N3Parser } from "n3"; +import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from "../../utils/wellKnownOntologies"; + +const SKIP_PREFIXES = new Set([ + "qudt", "unit", "dcterms", "dc", "org", "pmdco", "tto", + "iof-core", "iof-mat", "iof-qual", "schema", +]); + +async function fetchAndParse(url: string): Promise { + const resp = await fetch(url, { + headers: { Accept: "text/turtle, application/rdf+xml, application/n-triples, text/n3, */*;q=0.1" }, + redirect: "follow", + }); + + if (!resp.ok) { + throw new Error(`HTTP ${resp.status} from ${url}`); + } + + const body = await resp.text(); + const contentType = (resp.headers.get("content-type") || "").toLowerCase(); + + const isRdfXml = + contentType.includes("application/rdf+xml") || + contentType.includes("text/xml") || + contentType.includes("application/xml") || + url.endsWith(".rdf") || + url.endsWith(".owl") || + body.trimStart().startsWith("((resolve, reject) => { + const parser = new RdfXmlParser(); + let count = 0; + parser.on("data", () => { count++; }); + parser.on("end", () => resolve(count)); + parser.on("error", (err: Error) => reject(new Error(`RDF/XML parse error from ${url}: ${err.message}`))); + const readable = Readable.from([body]); + readable.pipe(parser); + }); + } + + // Try N3 (Turtle / N-Triples / N3 / TriG) + try { + const parser = new N3Parser(); + const quads = parser.parse(body); + return quads.length; + } catch (n3Err: any) { + throw new Error( + `Unknown RDF format from ${url} (content-type: ${contentType}): ${n3Err.message}`, + ); + } +} + +describe("well-known ontology reachability", () => { + for (const entry of WELL_KNOWN_PREFIXES) { + const loadUrl = resolveOntologyLoadUrl(entry.prefix); + const skip = SKIP_PREFIXES.has(entry.prefix); + + const testFn = skip ? it.skip : it; + + testFn( + `${entry.prefix} — ${loadUrl} resolves and parses as RDF`, + async () => { + const count = await fetchAndParse(loadUrl); + expect(count).toBeGreaterThan(0); + }, + { timeout: 30000 }, + ); + } +}); diff --git a/src/__tests__/utils/rdfManager.cors.test.ts b/src/__tests__/utils/rdfManager.cors.test.ts new file mode 100644 index 00000000..f94b2cf4 --- /dev/null +++ b/src/__tests__/utils/rdfManager.cors.test.ts @@ -0,0 +1,84 @@ +// @vitest-environment node +import { describe, it, expect, vi, afterEach } from "vitest"; +import { RDFManagerImpl } from "../../utils/rdfManager.impl"; + +const mockWorker = { + call: vi.fn(), + on: vi.fn(), + off: vi.fn(), +} as any; + +function makeInstance() { + return new RDFManagerImpl({ workerClient: mockWorker }); +} + +const TARGET = "https://example.org/test.ttl"; +const PROXY = "https://corsproxy.io/?url="; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("fetchWithCorsFallback", () => { + it("Scenario A: proxy activates on direct fetch failure", async () => { + const proxyResponse = new Response("ok", { status: 200 }); + const mockFetch = vi.fn() + .mockRejectedValueOnce(new TypeError("Failed to fetch")) + .mockResolvedValueOnce(proxyResponse); + vi.stubGlobal("fetch", mockFetch); + + const inst = makeInstance(); + const controller = new AbortController(); + const result = await (inst as any).fetchWithCorsFallback( + TARGET, + { Accept: "text/turtle" }, + controller.signal, + undefined, + PROXY, + ); + + expect(result).toBe(proxyResponse); + expect(mockFetch).toHaveBeenCalledTimes(2); + const secondCall = mockFetch.mock.calls[1]; + expect(secondCall[0]).toBe(PROXY + encodeURIComponent(TARGET)); + }); + + it("Scenario B: no proxy configured — error propagates", async () => { + const fetchError = new TypeError("Failed to fetch"); + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(fetchError)); + + const inst = makeInstance(); + const controller = new AbortController(); + await expect( + (inst as any).fetchWithCorsFallback( + TARGET, + {}, + controller.signal, + undefined, + "", + ), + ).rejects.toThrow("Failed to fetch"); + + expect(vi.mocked(fetch)).toHaveBeenCalledTimes(1); + }); + + it("Scenario C: direct fetch succeeds — proxy never called", async () => { + const directResponse = new Response("ok", { status: 200 }); + const mockFetch = vi.fn().mockResolvedValue(directResponse); + vi.stubGlobal("fetch", mockFetch); + + const inst = makeInstance(); + const controller = new AbortController(); + const result = await (inst as any).fetchWithCorsFallback( + TARGET, + {}, + controller.signal, + undefined, + PROXY, + ); + + expect(result).toBe(directResponse); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch.mock.calls[0][0]).toBe(TARGET); + }); +}); diff --git a/src/stores/ontologyStore.ts b/src/stores/ontologyStore.ts index a736497d..6079f41d 100644 --- a/src/stores/ontologyStore.ts +++ b/src/stores/ontologyStore.ts @@ -494,6 +494,7 @@ interface OntologyStore { concurrency?: number; onProgress?: (p: number, message: string) => void; forceDisabled?: boolean; + unconditional?: boolean; }, ) => Promise<{ candidates: string[]; @@ -843,6 +844,17 @@ export const useOntologyStore = create((set, get) => ({ /* ignore overall emit failures */ } + // Fire-and-forget owl:imports discovery into the ontology graph. + // Guard with !discovered to prevent mutual recursion: inner loadOntology calls + // triggered by discovery set discovered:true and must not re-trigger. + if (!options?.discovered) { + void get().discoverReferencedOntologies!({ + graphName: "urn:vg:ontologies", + load: "async", + unconditional: true, + }).catch(() => { /* ignore discovery errors */ }); + } + return { success: true, url: normRequestedUrl, canonicalUrl: canonicalNorm }; } catch (error: any) { try { @@ -1293,20 +1305,21 @@ export const useOntologyStore = create((set, get) => ({ concurrency?: number; onProgress?: (p: number, message: string) => void; forceDisabled?: boolean; + unconditional?: boolean; }) => { const opts = options || {}; // Session-level override (e.g. ?loadImports=false URL param). if (opts.forceDisabled === true) { return { candidates: [] }; } - // Respect the user setting — skip discovery entirely if disabled. + // Respect the user setting — skip discovery if disabled, unless the caller + // explicitly opts out (unconditional: true) for owl:imports following on explicit loads. const appCfgDiscover = useAppConfigStore.getState(); - if (appCfgDiscover && appCfgDiscover.config && appCfgDiscover.config.autoDiscoverOntologies === false) { + if (!opts.unconditional && appCfgDiscover && appCfgDiscover.config && appCfgDiscover.config.autoDiscoverOntologies === false) { return { candidates: [] }; } const requestedGraphName = typeof opts.graphName === "string" && opts.graphName.trim() ? opts.graphName : "urn:vg:data"; - const graphName = "urn:vg:data"; const loadMode = typeof opts.load === "undefined" ? "async" : opts.load; const timeoutMs = typeof opts.timeoutMs === "number" ? opts.timeoutMs : 15000; const concurrency = typeof opts.concurrency === "number" ? opts.concurrency : 6; @@ -1371,14 +1384,13 @@ export const useOntologyStore = create((set, get) => ({ }; })(); - const vgM = vgMeasureModule("discoverReferencedOntologies", { graphName, loadMode, timeoutMs, concurrency }); + const vgM = vgMeasureModule("discoverReferencedOntologies", { graphName: requestedGraphName, loadMode, timeoutMs, concurrency }); console.debug("[VG_DEBUG] discoverReferencedOntologies.invoked", { - graphName, + graphName: requestedGraphName, loadMode, timeoutMs, concurrency, - requestedGraphName, }); const mgr = get().rdfManager; @@ -1391,13 +1403,13 @@ export const useOntologyStore = create((set, get) => ({ const typeQuads = await fetchSerializedQuads( mgr, - graphName, + requestedGraphName, { predicate: RDF_TYPE }, 2000, ); const importQuads = await fetchSerializedQuads( mgr, - graphName, + requestedGraphName, { predicate: OWL_IMPORTS }, 2000, ); @@ -1460,8 +1472,7 @@ export const useOntologyStore = create((set, get) => ({ } console.debug("[VG_DEBUG] discoverReferencedOntologies.candidates", { - graph: graphName, - requestedGraphName, + graph: requestedGraphName, candidates, sources: candidateSources, }); @@ -1512,7 +1523,7 @@ export const useOntologyStore = create((set, get) => ({ vgM && typeof vgM.end === "function" && vgM.end({ reason: "no_mgr_emit" }); throw new Error("discoverReferencedOntologies: no rdfManager.emitAllSubjects available"); } - await (mgrInst as any).emitAllSubjects("urn:vg:data"); + await (mgrInst as any).emitAllSubjects(requestedGraphName); onProgress && onProgress(100, "Discovery complete"); vgM && typeof vgM.end === "function" && vgM.end({ reason: "complete", resultsCount: results.length }); diff --git a/vitest.config.ts b/vitest.config.ts index 6ed1dedd..444f2ae0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,7 +22,12 @@ export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['src/test-setup.ts'], - exclude: ['.trunk/**', 'node_modules/**', 'e2e/**'], + exclude: [ + '.trunk/**', + 'node_modules/**', + 'e2e/**', + ...(process.env.NETWORK_TESTS ? [] : ['**/*.network.test.ts']), + ], server: { deps: { inline: ['@reactodia/workspace'], From df0894b91d107bbd0ef6f1b42d6965bc13277643 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:43:32 +0200 Subject: [PATCH 02/22] fix(ontologies): add ontologyUrl for tto/schema/pmdco, un-skip network tests - tto: GitHub Turtle (w3id.org namespace returns 404) - schema: schema.org Turtle snapshot (namespace returns HTML) - pmdco: GitHub Turtle (more reliable than w3id RDF/XML redirect) - Skip list reduced to iof-mat + iof-qual (all known URLs 404) --- src/__tests__/stores/wellKnownOntologies.network.test.ts | 8 +++----- src/utils/wellKnownOntologies.ts | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/__tests__/stores/wellKnownOntologies.network.test.ts b/src/__tests__/stores/wellKnownOntologies.network.test.ts index 2ebd60f7..942fdc8c 100644 --- a/src/__tests__/stores/wellKnownOntologies.network.test.ts +++ b/src/__tests__/stores/wellKnownOntologies.network.test.ts @@ -4,10 +4,8 @@ import { Readable } from "node:stream"; import { Parser as N3Parser } from "n3"; import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from "../../utils/wellKnownOntologies"; -const SKIP_PREFIXES = new Set([ - "qudt", "unit", "dcterms", "dc", "org", "pmdco", "tto", - "iof-core", "iof-mat", "iof-qual", "schema", -]); +// iof-mat and iof-qual: all known spec + GitHub URLs return 404 as of 2026-04 +const SKIP_PREFIXES = new Set(["iof-mat", "iof-qual"]); async function fetchAndParse(url: string): Promise { const resp = await fetch(url, { @@ -65,11 +63,11 @@ describe("well-known ontology reachability", () => { testFn( `${entry.prefix} — ${loadUrl} resolves and parses as RDF`, + { timeout: 30000 }, async () => { const count = await fetchAndParse(loadUrl); expect(count).toBeGreaterThan(0); }, - { timeout: 30000 }, ); } }); diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index ec3c962f..ef7b3b66 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -88,11 +88,13 @@ export const WELL_KNOWN_PREFIXES = [ prefix: "pmdco", url: "https://w3id.org/pmd/co/", name: "PMD Core", + ontologyUrl: "https://raw.githubusercontent.com/materialdigital/core-ontology/main/pmdco.ttl", }, { prefix: "tto", url: "https://w3id.org/pmd/ao/tto/", name: "PMD Tensile Test", + ontologyUrl: "https://raw.githubusercontent.com/materialdigital/tensile-test-ontology/main/tto.ttl", }, { prefix: "iof-core", @@ -149,6 +151,7 @@ export const WELL_KNOWN_PREFIXES = [ prefix: "schema", url: "https://schema.org/", name: "Schema.org", + ontologyUrl: "https://schema.org/version/latest/schemaorg-current-https.ttl", }, { prefix: "sh", From 985fe00089add6469ec8061a16f09db5ce4861ee Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:44:49 +0200 Subject: [PATCH 03/22] chore(ontologies): remove iof-mat and iof-qual (all URLs 404) --- .../stores/wellKnownOntologies.network.test.ts | 8 +------- src/utils/wellKnownOntologies.ts | 10 ---------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/__tests__/stores/wellKnownOntologies.network.test.ts b/src/__tests__/stores/wellKnownOntologies.network.test.ts index 942fdc8c..4faef8be 100644 --- a/src/__tests__/stores/wellKnownOntologies.network.test.ts +++ b/src/__tests__/stores/wellKnownOntologies.network.test.ts @@ -4,9 +4,6 @@ import { Readable } from "node:stream"; import { Parser as N3Parser } from "n3"; import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from "../../utils/wellKnownOntologies"; -// iof-mat and iof-qual: all known spec + GitHub URLs return 404 as of 2026-04 -const SKIP_PREFIXES = new Set(["iof-mat", "iof-qual"]); - async function fetchAndParse(url: string): Promise { const resp = await fetch(url, { headers: { Accept: "text/turtle, application/rdf+xml, application/n-triples, text/n3, */*;q=0.1" }, @@ -57,11 +54,8 @@ async function fetchAndParse(url: string): Promise { describe("well-known ontology reachability", () => { for (const entry of WELL_KNOWN_PREFIXES) { const loadUrl = resolveOntologyLoadUrl(entry.prefix); - const skip = SKIP_PREFIXES.has(entry.prefix); - - const testFn = skip ? it.skip : it; - testFn( + it( `${entry.prefix} — ${loadUrl} resolves and parses as RDF`, { timeout: 30000 }, async () => { diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index ef7b3b66..a4f036b1 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -101,16 +101,6 @@ export const WELL_KNOWN_PREFIXES = [ url: "https://spec.industrialontologies.org/ontology/core/Core/", name: "IOF Core", }, - { - prefix: "iof-mat", - url: "https://spec.industrialontologies.org/ontology/materials/Materials/", - name: "IOF Materials", - }, - { - prefix: "iof-qual", - url: "https://spec.industrialontologies.org/ontology/qualities/", - name: "IOF Qualities", - }, { prefix: "ro", url: "http://purl.obolibrary.org/obo/RO_", From aeaf76e0ef0a7515f2668d7a8bba12f4e21a5bde Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:51:38 +0200 Subject: [PATCH 04/22] fix(ui): prevent dialog closing when selecting autocomplete dropdown item Radix Dialog fires onPointerDownOutside for portal elements outside its DOM subtree, dismissing the dialog before mousedown completes on the dropdown item. Mark the portal ul with data-autocomplete-portal and intercept the outside-pointer event on DialogContent. --- src/components/Canvas/ReactodiaCanvas.tsx | 7 ++++++- src/components/ui/OntologyUrlAutoComplete.tsx | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index 8d3a6be2..ff7eef6a 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -1417,7 +1417,12 @@ export default function ReactodiaCanvas() { /> - + { + if ((e.target as Element)?.closest('[data-autocomplete-portal]')) e.preventDefault(); + }} + > Load Ontology diff --git a/src/components/ui/OntologyUrlAutoComplete.tsx b/src/components/ui/OntologyUrlAutoComplete.tsx index 59c16bc1..b3700b60 100644 --- a/src/components/ui/OntologyUrlAutoComplete.tsx +++ b/src/components/ui/OntologyUrlAutoComplete.tsx @@ -67,6 +67,7 @@ export default function OntologyUrlAutoComplete({ value, onChange, placeholder, ref={listRef} role="listbox" style={{ position: 'fixed', top: dropPos.top, left: dropPos.left, width: dropPos.width, zIndex: 9999 }} + data-autocomplete-portal="" className="max-h-60 overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md py-1" > {filtered.map((e, i) => ( From dcd107b495e48353ff81823be1943762c52bac3b Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:52:47 +0200 Subject: [PATCH 05/22] fix(ui): use originalEvent.target for autocomplete portal detection onPointerDownOutside wraps the native event; e.target points to the dialog element, not the clicked node. Use e.detail.originalEvent.target. --- src/components/Canvas/ReactodiaCanvas.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index ff7eef6a..0827914a 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -1420,7 +1420,8 @@ export default function ReactodiaCanvas() { { - if ((e.target as Element)?.closest('[data-autocomplete-portal]')) e.preventDefault(); + const target = e.detail.originalEvent.target as Element | null; + if (target?.closest('[data-autocomplete-portal]')) e.preventDefault(); }} > From 5a15bddb0664af2c125a7c76ebd889555f1dcaf0 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:58:46 +0200 Subject: [PATCH 06/22] Revert "fix(ui): use originalEvent.target for autocomplete portal detection" This reverts commit dcd107b495e48353ff81823be1943762c52bac3b. --- src/components/Canvas/ReactodiaCanvas.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index 0827914a..ff7eef6a 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -1420,8 +1420,7 @@ export default function ReactodiaCanvas() { { - const target = e.detail.originalEvent.target as Element | null; - if (target?.closest('[data-autocomplete-portal]')) e.preventDefault(); + if ((e.target as Element)?.closest('[data-autocomplete-portal]')) e.preventDefault(); }} > From 986d7577e75bf1a7bcccb08afda59bac67bded90 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 08:58:46 +0200 Subject: [PATCH 07/22] Revert "fix(ui): prevent dialog closing when selecting autocomplete dropdown item" This reverts commit aeaf76e0ef0a7515f2668d7a8bba12f4e21a5bde. --- src/components/Canvas/ReactodiaCanvas.tsx | 7 +------ src/components/ui/OntologyUrlAutoComplete.tsx | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index ff7eef6a..8d3a6be2 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -1417,12 +1417,7 @@ export default function ReactodiaCanvas() { /> - { - if ((e.target as Element)?.closest('[data-autocomplete-portal]')) e.preventDefault(); - }} - > + Load Ontology diff --git a/src/components/ui/OntologyUrlAutoComplete.tsx b/src/components/ui/OntologyUrlAutoComplete.tsx index b3700b60..59c16bc1 100644 --- a/src/components/ui/OntologyUrlAutoComplete.tsx +++ b/src/components/ui/OntologyUrlAutoComplete.tsx @@ -67,7 +67,6 @@ export default function OntologyUrlAutoComplete({ value, onChange, placeholder, ref={listRef} role="listbox" style={{ position: 'fixed', top: dropPos.top, left: dropPos.left, width: dropPos.width, zIndex: 9999 }} - data-autocomplete-portal="" className="max-h-60 overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md py-1" > {filtered.map((e, i) => ( From 31a864c10f9a4ee5a109a1d343e6b5cea0525a9a Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:05:43 +0200 Subject: [PATCH 08/22] fix(ui): stop pointerdown propagation on autocomplete items to prevent Radix dialog dismissal --- src/components/ui/OntologyUrlAutoComplete.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ui/OntologyUrlAutoComplete.tsx b/src/components/ui/OntologyUrlAutoComplete.tsx index 59c16bc1..f9ae7c62 100644 --- a/src/components/ui/OntologyUrlAutoComplete.tsx +++ b/src/components/ui/OntologyUrlAutoComplete.tsx @@ -74,6 +74,7 @@ export default function OntologyUrlAutoComplete({ value, onChange, placeholder, key={e.url} role="option" aria-selected={i === activeIndex} + onPointerDown={ev => ev.stopPropagation()} onMouseDown={ev => { ev.preventDefault(); handleSelect(e.url); }} onMouseEnter={() => setActiveIndex(i)} className={cn( From 44e82011ac9306d3f0a00b682c649f32fa052796 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:10:05 +0200 Subject: [PATCH 09/22] fix(ui): replace Radix Dialog with plain modal for Load Ontology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Radix DismissableLayer intercepts pointerdown on portaled dropdown items regardless of stopPropagation attempts, closing the dialog before selection completes. Plain div modal has no such listener — autocomplete works correctly. --- src/components/Canvas/ReactodiaCanvas.tsx | 100 ++++++++++-------- src/components/ui/OntologyUrlAutoComplete.tsx | 1 - 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index 8d3a6be2..f453074c 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -38,7 +38,6 @@ import type { AppConfig } from '@/stores/appConfigStore'; import { LayoutPopover } from './LayoutPopover'; import { RdfPropertyEditor } from './rdfPropertyEditor'; import { toast } from 'sonner'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog'; import { Label } from '../ui/label'; import { Input } from '../ui/input'; import OntologyUrlAutoComplete from '../ui/OntologyUrlAutoComplete'; @@ -1416,53 +1415,64 @@ export default function ReactodiaCanvas() { onOpenChange={setSettingsOpen} /> - - - - Load Ontology - - Enter a URL or type to search well-known ontologies. - - -
-
- - + {loadOntologyOpen && ( +
{ if (e.target === e.currentTarget) setLoadOntologyOpen(false); }} + onKeyDown={(e) => { if (e.key === 'Escape') setLoadOntologyOpen(false); }} + > +
+ +
+

Load Ontology

+

Enter a URL or type to search well-known ontologies.

-
- - +
+
+ + +
+
+ + +
- -
+ + )} ev.stopPropagation()} onMouseDown={ev => { ev.preventDefault(); handleSelect(e.url); }} onMouseEnter={() => setActiveIndex(i)} className={cn( From 2cbe6b934dae7cdfe55b98e7a68e7e6c36dc2685 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:14:42 +0200 Subject: [PATCH 10/22] fix(ui): show correct load status in ontology widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failed entries showed green 'Loaded' because TopBar never checked loadStatus. Badge count also included failed entries. Now: - badge counts only non-failed entries - widget shows red 'Failed' (with error tooltip), muted 'Loading…', or green 'Loaded' --- src/components/Canvas/ReactodiaCanvas.tsx | 4 +++- src/components/Canvas/TopBar.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index f453074c..ea7bdfa3 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -376,7 +376,9 @@ export default function ReactodiaCanvas() { // Resolved once at mount from ?loadImports URL param. false = imports disabled for this session. const loadImportsEnabledRef = React.useRef(true); - const ontologyCount = useOntologyStore(s => s.loadedOntologies?.length ?? 0); + const ontologyCount = useOntologyStore(s => + (s.loadedOntologies ?? []).filter((o: any) => o.loadStatus !== 'fail').length + ); const namespaces = useOntologyStore(s => Array.isArray(s.namespaceRegistry) ? s.namespaceRegistry : []); const loadKnowledgeGraph = useOntologyStore(s => s.loadKnowledgeGraph); const loadAdditionalOntologies = useOntologyStore(s => s.loadAdditionalOntologies); diff --git a/src/components/Canvas/TopBar.tsx b/src/components/Canvas/TopBar.tsx index c07ce100..f01ca729 100644 --- a/src/components/Canvas/TopBar.tsx +++ b/src/components/Canvas/TopBar.tsx @@ -145,7 +145,12 @@ export const TopBar: React.FC = ({
{ont?.name || 'Unknown'}
{ontologyUrl || 'No URI'}
- Loaded + {ont?.loadStatus === 'fail' + ? Failed + : ont?.loadStatus === 'pending' + ? Loading… + : Loaded + } {isAutoloaded && · autoload} {isCore && · core}
From 125c7179b2051b23211078c71f3de6f6f8cd36e1 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:17:40 +0200 Subject: [PATCH 11/22] fix(ontology): resolve ontologyUrl before fetching in loadOntology loadOntology was fetching the namespace URL directly (e.g. w3id.org/pmd/ao/tto/) instead of the actual RDF document URL. Call resolveOntologyLoadUrl to get the correct fetch URL while keeping normRequestedUrl as the canonical store key. --- src/stores/ontologyStore.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/ontologyStore.ts b/src/stores/ontologyStore.ts index 6079f41d..d5bde8f1 100644 --- a/src/stores/ontologyStore.ts +++ b/src/stores/ontologyStore.ts @@ -11,7 +11,7 @@ import { RDFManager, rdfManager } from "../utils/rdfManager"; import { useAppConfigStore } from "./appConfigStore"; import { useSettingsStore } from "./settingsStore"; import { debug, info, warn, error, fallback } from "../utils/startupDebug"; -import { WELL_KNOWN, WELL_KNOWN_PREFIXES } from "../utils/wellKnownOntologies"; +import { WELL_KNOWN, WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from "../utils/wellKnownOntologies"; import { DataFactory, Quad } from "n3"; import { toast } from "sonner"; import { buildPaletteMap } from "../components/Canvas/core/namespacePalette"; @@ -619,6 +619,8 @@ export const useOntologyStore = create((set, get) => ({ // normalize requested URL (http -> https, trim) const normRequestedUrl = normalizeOntologyUri(url); + // Resolve to the actual fetch URL (ontologyUrl overrides namespace URL for well-known entries) + const fetchUrl = normalizeOntologyUri(resolveOntologyLoadUrl(normRequestedUrl)); let canonicalNorm: string | undefined = undefined; // If well-known, register a lightweight entry so UI shows it immediately. @@ -663,10 +665,10 @@ export const useOntologyStore = create((set, get) => ({ const corsProxyUrl = useSettingsStore.getState().settings.corsProxyUrl; const loadOpts = { timeoutMs: 15000, corsProxyUrl: corsProxyUrl || undefined }; if (mgr && typeof (mgr as any).loadRDFFromUrl === "function") { - await (mgr as any).loadRDFFromUrl(normRequestedUrl, "urn:vg:ontologies", loadOpts); + await (mgr as any).loadRDFFromUrl(fetchUrl, "urn:vg:ontologies", loadOpts); } else { // fallback to module-level manager - await (rdfManager as any).loadRDFFromUrl(normRequestedUrl, "urn:vg:ontologies", loadOpts); + await (rdfManager as any).loadRDFFromUrl(fetchUrl, "urn:vg:ontologies", loadOpts); } } catch (err) { warn( From 77ff0dd8b23bd383692d37612572ae1d66b4ebf1 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:21:11 +0200 Subject: [PATCH 12/22] fix(ontology): also try http form when resolving fetch URL for well-known entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeOntologyUri upgrades http→https before resolveOntologyLoadUrl runs, but WELL_KNOWN entries store http:// namespace URLs. Try the http form as fallback so entries like foaf (url: http://xmlns.com/foaf/0.1/) resolve correctly to their ontologyUrl. --- src/stores/ontologyStore.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/stores/ontologyStore.ts b/src/stores/ontologyStore.ts index d5bde8f1..5df059e7 100644 --- a/src/stores/ontologyStore.ts +++ b/src/stores/ontologyStore.ts @@ -619,8 +619,13 @@ export const useOntologyStore = create((set, get) => ({ // normalize requested URL (http -> https, trim) const normRequestedUrl = normalizeOntologyUri(url); - // Resolve to the actual fetch URL (ontologyUrl overrides namespace URL for well-known entries) - const fetchUrl = normalizeOntologyUri(resolveOntologyLoadUrl(normRequestedUrl)); + // Resolve to the actual fetch URL (ontologyUrl overrides namespace URL for well-known entries). + // Try both the normalized (https) form and the original http form because WELL_KNOWN urls use http://. + const _httpForm0 = normRequestedUrl.replace(/^https:\/\//i, "http://"); + const _resolved0 = resolveOntologyLoadUrl(normRequestedUrl); + const fetchUrl = normalizeOntologyUri( + _resolved0 !== normRequestedUrl ? _resolved0 : resolveOntologyLoadUrl(_httpForm0) + ); let canonicalNorm: string | undefined = undefined; // If well-known, register a lightweight entry so UI shows it immediately. From dc928bc8c403f7d258a4975f893becc333e0b871 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:29:46 +0200 Subject: [PATCH 13/22] feat(ontologies): add W3C recommended ontologies to well-known registry vcard, ldp, oa, odrl, as (Activity Streams), csvw, locn, wgs84, qb (RDF Data Cube) All 37 entries resolve and parse as RDF (network test 37/37 pass). --- src/utils/wellKnownOntologies.ts | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index a4f036b1..0f49ea9b 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -155,6 +155,60 @@ export const WELL_KNOWN_PREFIXES = [ name: "OWL-Time - Time Ontology in OWL", ontologyUrl: "https://www.w3.org/2006/time", }, + { + prefix: "vcard", + url: "http://www.w3.org/2006/vcard/ns#", + name: "vCard Ontology", + ontologyUrl: "https://www.w3.org/2006/vcard/ns", + }, + { + prefix: "ldp", + url: "http://www.w3.org/ns/ldp#", + name: "LDP - Linked Data Platform", + ontologyUrl: "https://www.w3.org/ns/ldp", + }, + { + prefix: "oa", + url: "http://www.w3.org/ns/oa#", + name: "OA - Web Annotation Vocabulary", + ontologyUrl: "https://www.w3.org/ns/oa", + }, + { + prefix: "odrl", + url: "http://www.w3.org/ns/odrl/2/", + name: "ODRL - Open Digital Rights Language", + ontologyUrl: "https://www.w3.org/ns/odrl/2/", + }, + { + prefix: "as", + url: "https://www.w3.org/ns/activitystreams#", + name: "Activity Streams 2.0", + ontologyUrl: "https://www.w3.org/ns/activitystreams-owl.ttl", + }, + { + prefix: "csvw", + url: "http://www.w3.org/ns/csvw#", + name: "CSVW - CSV on the Web", + ontologyUrl: "https://www.w3.org/ns/csvw", + }, + { + prefix: "locn", + url: "http://www.w3.org/ns/locn#", + name: "LOCN - Core Location Vocabulary", + ontologyUrl: "https://www.w3.org/ns/locn", + }, + { + prefix: "wgs84", + url: "http://www.w3.org/2003/01/geo/wgs84_pos#", + name: "WGS84 - Basic Geo Vocabulary", + ontologyUrl: "http://www.w3.org/2003/01/geo/wgs84_pos.rdf", + }, + { + prefix: "qb", + url: "http://purl.org/linked-data/cube#", + name: "RDF Data Cube Vocabulary", + ontologyUrl: "https://raw.githubusercontent.com/UKGovLD/publishing-statistical-data/master/specs/src/main/vocab/cube.ttl", + }, ] as const; export const WELL_KNOWN_BY_PREFIX: Record< From 60a9c5e82545b9acea7543855182c9ba3d4daa53 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 09:50:15 +0200 Subject: [PATCH 14/22] feat(ontologies): expand registry with 18 new ontologies + description field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add well-known prefixes for: ical, bibo, fabio, cito, vann, mo, exif, gr, cc, sioc, saref, bot, geo, pav, adms, regorg, doap, dcmitype. Add `description` field to every entry (what the ontology models). Autocomplete now filters on description so typing "calendar" surfaces ical, "music" surfaces mo, etc. Dropdown now shows description + resolved fetch URL alongside the prefix and name. Skipped: EBU Core (403), GS1 (403), legalcore (404), OM2 (500), HL7 FHIR (6 MB file — risk of test timeout), BBC rNews (404). All 55 network tests pass. --- src/components/ui/OntologyUrlAutoComplete.tsx | 10 +- src/utils/wellKnownOntologies.ts | 214 +++++++++++++++++- 2 files changed, 215 insertions(+), 9 deletions(-) diff --git a/src/components/ui/OntologyUrlAutoComplete.tsx b/src/components/ui/OntologyUrlAutoComplete.tsx index 59c16bc1..48312b01 100644 --- a/src/components/ui/OntologyUrlAutoComplete.tsx +++ b/src/components/ui/OntologyUrlAutoComplete.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useLayoutEffect, useEffect, useMemo } from 'react'; import ReactDOM from 'react-dom'; -import { WELL_KNOWN_PREFIXES } from '../../utils/wellKnownOntologies'; +import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from '../../utils/wellKnownOntologies'; import { cn } from '../../lib/utils'; interface Props { @@ -25,7 +25,8 @@ export default function OntologyUrlAutoComplete({ value, onChange, placeholder, return WELL_KNOWN_PREFIXES.filter(e => e.prefix.toLowerCase().includes(q) || e.name.toLowerCase().includes(q) || - e.url.toLowerCase().includes(q) + e.url.toLowerCase().includes(q) || + ((e as any).description as string | undefined)?.toLowerCase().includes(q) ); }, [query]); @@ -82,7 +83,10 @@ export default function OntologyUrlAutoComplete({ value, onChange, placeholder, )} >
{e.prefix} — {e.name}
-
{e.url}
+ {(e as any).description && ( +
{(e as any).description}
+ )} +
{resolveOntologyLoadUrl(e.prefix)}
))} , diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index 0f49ea9b..125fd51e 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -2,7 +2,7 @@ * Centralized well-known ontology and prefix mappings. * * New canonical structure: - * - WELL_KNOWN_PREFIXES: Array of { prefix, url, name } + * - WELL_KNOWN_PREFIXES: Array of { prefix, url, name, description } * - WELL_KNOWN_BY_PREFIX: Record * - WELL_KNOWN_BY_URL: Map * @@ -17,203 +17,405 @@ export const WELL_KNOWN_PREFIXES = [ prefix: "rdf", url: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", name: "RDF - The RDF Concepts Vocabulary", + description: "Fundamental RDF concepts: resources, properties, literals and statements", isCore: true, }, { prefix: "rdfs", url: "http://www.w3.org/2000/01/rdf-schema#", name: "RDFS - The RDF Schema Vocabulary", + description: "RDF Schema: classes, properties, subclass and subproperty hierarchies", isCore: true, }, - { prefix: "owl", url: "http://www.w3.org/2002/07/owl#", name: "OWL", isCore: true }, - { prefix: "xsd", url: "http://www.w3.org/2001/XMLSchema#", name: "XSD", isCore: true }, - { prefix: "skos", url: "http://www.w3.org/2004/02/skos/core#", name: "SKOS" }, + { + prefix: "owl", + url: "http://www.w3.org/2002/07/owl#", + name: "OWL", + description: "Web Ontology Language: expressive class and property axioms, reasoning", + isCore: true, + }, + { + prefix: "xsd", + url: "http://www.w3.org/2001/XMLSchema#", + name: "XSD", + description: "XML Schema Datatypes: string, integer, date, boolean and other primitives", + isCore: true, + }, + { + prefix: "skos", + url: "http://www.w3.org/2004/02/skos/core#", + name: "SKOS", + description: "Simple Knowledge Organization System: thesauri, taxonomies and classification schemes", + }, { prefix: "prov", url: "http://www.w3.org/ns/prov#", name: "PROV-O - The PROV Ontology", + description: "Provenance: entities, activities, agents and their causal relationships", ontologyUrl: "https://www.w3.org/ns/prov-o", }, { prefix: "p-plan", url: "http://purl.org/net/p-plan#", name: "P-Plan - The P-Plan Ontology", + description: "Scientific workflow provenance: plans, steps and entities extending PROV-O", ontologyUrl: "https://purl.org/net/p-plan", }, { prefix: "bfo", url: "http://purl.obolibrary.org/obo/BFO_", name: "BFO 2 - Basic Formal Ontology 2.0", + description: "Upper-level foundational ontology for biomedical and scientific domains", ontologyUrl: "https://purl.obolibrary.org/obo/bfo/2.0/bfo.owl", }, { prefix: "bfo2020", url: "https://basic-formal-ontology.org/2020/formulas/owl/", name: "BFO 2020 - Basic Formal Ontology 2020", + description: "ISO/IEC 21838-compliant update of Basic Formal Ontology", ontologyUrl: "https://raw.githubusercontent.com/BFO-ontology/BFO-2020/master/21838-2/owl/bfo-core.owl", }, { prefix: "dcat", url: "http://www.w3.org/ns/dcat#", name: "DCAT - Data Catalog Vocabulary", + description: "Data catalogs: datasets, distributions, data services and catalog records", ontologyUrl: "https://www.w3.org/ns/dcat2", }, { prefix: "qudt", url: "http://qudt.org/schema/qudt/", name: "QUDT - Quantities, Units, Dimensions and Types", + description: "Measurement schema: quantity kinds, units, dimensions and numeric values", }, { prefix: "unit", url: "http://qudt.org/vocab/unit/", name: "QUDT Units Vocabulary", + description: "Concrete SI and non-SI units of measure (metre, kilogram, second, …)", }, { prefix: "dcterms", url: "http://purl.org/dc/terms/", name: "Dublin Core Terms", + description: "General metadata: titles, creators, dates, subjects and descriptions", }, { prefix: "dc", url: "http://purl.org/dc/elements/1.1/", name: "Dublin Core", + description: "Legacy Dublin Core metadata elements (title, creator, date, format, …)", + }, + { + prefix: "foaf", + url: "http://xmlns.com/foaf/0.1/", + name: "FOAF", + description: "Friend of a Friend: people, organizations, social networks and accounts", + ontologyUrl: "https://xmlns.com/foaf/spec/index.rdf", }, - { prefix: "foaf", url: "http://xmlns.com/foaf/0.1/", name: "FOAF", ontologyUrl: "https://xmlns.com/foaf/spec/index.rdf" }, { prefix: "org", url: "http://www.w3.org/ns/org#", name: "Organization", + description: "Formal organizations, membership, roles, sites and organizational units", }, { prefix: "pmdco", url: "https://w3id.org/pmd/co/", name: "PMD Core", + description: "Materials science and engineering: processes, specimens and characteristics", ontologyUrl: "https://raw.githubusercontent.com/materialdigital/core-ontology/main/pmdco.ttl", }, { prefix: "tto", url: "https://w3id.org/pmd/ao/tto/", name: "PMD Tensile Test", + description: "Mechanical tensile testing: specimens, force, elongation and test procedures", ontologyUrl: "https://raw.githubusercontent.com/materialdigital/tensile-test-ontology/main/tto.ttl", }, { prefix: "iof-core", url: "https://spec.industrialontologies.org/ontology/core/Core/", name: "IOF Core", + description: "Industrial ontology: processes, equipment, capabilities and maintenance", }, { prefix: "ro", url: "http://purl.obolibrary.org/obo/RO_", name: "RO - Relations Ontology", + description: "Biological and biomedical relation types: part-of, has-role, regulates, …", ontologyUrl: "https://purl.obolibrary.org/obo/ro.owl", }, { prefix: "iao", url: "http://purl.obolibrary.org/obo/IAO_", name: "IAO - Information Artifact Ontology", + description: "Information entities: documents, data items, measurement data and identifiers", ontologyUrl: "https://purl.obolibrary.org/obo/iao.owl", }, { prefix: "log", url: "https://w3id.org/pmd/log/", name: "PMD LOG - PMD Laboratory Operations Graph", + description: "Laboratory workflows: instruments, measurements, samples and operations", ontologyUrl: "https://w3id.org/pmd/log", }, { prefix: "emmo", url: "https://w3id.org/emmo#", name: "EMMO - European Materials Modelling Ontology", + description: "Physics, chemistry and materials modelling: properties, models and simulations", ontologyUrl: "https://raw.githubusercontent.com/emmo-repo/EMMO/master/emmo.ttl", }, { prefix: "sosa", url: "http://www.w3.org/ns/sosa/", name: "SOSA - Sensor, Observation, Sample and Actuator", + description: "IoT observation framework: sensors, actuators, samples and observations", ontologyUrl: "https://www.w3.org/ns/sosa/", }, { prefix: "ssn", url: "http://www.w3.org/ns/ssn/", name: "SSN - Semantic Sensor Network Ontology", + description: "Semantic sensor networks: systems, platforms, capabilities and deployments", ontologyUrl: "https://www.w3.org/ns/ssn/", }, { prefix: "schema", url: "https://schema.org/", name: "Schema.org", + description: "Events, people, places, products, organizations and web content markup", ontologyUrl: "https://schema.org/version/latest/schemaorg-current-https.ttl", }, { prefix: "sh", url: "http://www.w3.org/ns/shacl#", name: "SHACL - Shapes Constraint Language", + description: "Shapes and constraints for RDF graph validation and conformance testing", ontologyUrl: "https://www.w3.org/ns/shacl", }, { prefix: "time", url: "http://www.w3.org/2006/time#", name: "OWL-Time - Time Ontology in OWL", + description: "Temporal entities: instants, intervals, durations, time positions and zones", ontologyUrl: "https://www.w3.org/2006/time", }, { prefix: "vcard", url: "http://www.w3.org/2006/vcard/ns#", name: "vCard Ontology", + description: "Electronic business cards: contact details, addresses, telephones and emails", ontologyUrl: "https://www.w3.org/2006/vcard/ns", }, { prefix: "ldp", url: "http://www.w3.org/ns/ldp#", name: "LDP - Linked Data Platform", + description: "RESTful Linked Data containers, resources and interactions over HTTP", ontologyUrl: "https://www.w3.org/ns/ldp", }, { prefix: "oa", url: "http://www.w3.org/ns/oa#", name: "OA - Web Annotation Vocabulary", + description: "Web annotations: bodies, targets, selectors and annotation motivations", ontologyUrl: "https://www.w3.org/ns/oa", }, { prefix: "odrl", url: "http://www.w3.org/ns/odrl/2/", name: "ODRL - Open Digital Rights Language", + description: "Rights management: policies, permissions, prohibitions and obligations", ontologyUrl: "https://www.w3.org/ns/odrl/2/", }, { prefix: "as", url: "https://www.w3.org/ns/activitystreams#", name: "Activity Streams 2.0", + description: "Social activities: actors, objects, likes, follows and activity feeds", ontologyUrl: "https://www.w3.org/ns/activitystreams-owl.ttl", }, { prefix: "csvw", url: "http://www.w3.org/ns/csvw#", name: "CSVW - CSV on the Web", + description: "Tabular data: CSV file metadata, column definitions and data types", ontologyUrl: "https://www.w3.org/ns/csvw", }, { prefix: "locn", url: "http://www.w3.org/ns/locn#", name: "LOCN - Core Location Vocabulary", + description: "Addresses, postal codes and geographic locations for government data", ontologyUrl: "https://www.w3.org/ns/locn", }, { prefix: "wgs84", url: "http://www.w3.org/2003/01/geo/wgs84_pos#", name: "WGS84 - Basic Geo Vocabulary", + description: "Geographic coordinates: latitude, longitude and altitude in WGS84", ontologyUrl: "http://www.w3.org/2003/01/geo/wgs84_pos.rdf", }, { prefix: "qb", url: "http://purl.org/linked-data/cube#", name: "RDF Data Cube Vocabulary", + description: "Statistical data: observations, measures, dimensions and attributes", ontologyUrl: "https://raw.githubusercontent.com/UKGovLD/publishing-statistical-data/master/specs/src/main/vocab/cube.ttl", }, + + // Calendar & events + { + prefix: "ical", + url: "http://www.w3.org/2002/12/cal/ical#", + name: "iCal - iCalendar Vocabulary", + description: "Calendar events: meetings, recurring schedules, alarms and calendar components", + ontologyUrl: "http://www.w3.org/2002/12/cal/ical.rdf", + }, + + // Bibliographic & scholarly + { + prefix: "bibo", + url: "http://purl.org/ontology/bibo/", + name: "BIBO - Bibliographic Ontology", + description: "Bibliographic metadata: books, articles, theses, reports and academic works", + }, + { + prefix: "fabio", + url: "http://purl.org/spar/fabio/", + name: "FABIO - FRBR-aligned Bibliographic Ontology", + description: "Publishing works: journal articles, conference papers, datasets and expressions", + }, + { + prefix: "cito", + url: "http://purl.org/spar/cito/", + name: "CiTO - Citation Typing Ontology", + description: "Scholarly citation types: supports, disputes, extends, documents and cites", + }, + { + prefix: "vann", + url: "http://purl.org/vocab/vann/", + name: "VANN - Vocabulary for Annotating Vocabulary Descriptions", + description: "Vocabulary metadata: preferred namespace prefixes and example usage", + ontologyUrl: "https://lov.linkeddata.es/dataset/lov/vocabs/vann/versions/2010-06-07.n3", + }, + + // Music + { + prefix: "mo", + url: "http://purl.org/ontology/mo/", + name: "MO - Music Ontology", + description: "Music metadata: tracks, albums, artists, performances and recordings", + ontologyUrl: "https://raw.githubusercontent.com/motools/musicontology/master/rdf/musicontology.n3", + }, + + // Media & images + { + prefix: "exif", + url: "http://www.w3.org/2003/12/exif/ns#", + name: "EXIF - Exif Vocabulary", + description: "Image metadata: camera settings, GPS coordinates, exposure and orientation", + ontologyUrl: "http://www.w3.org/2003/12/exif/ns", + }, + + // E-commerce & products + { + prefix: "gr", + url: "http://purl.org/goodrelations/v1#", + name: "GoodRelations - E-commerce Ontology", + description: "E-commerce: products, offerings, prices, businesses and delivery options", + ontologyUrl: "http://purl.org/goodrelations/v1", + }, + + // Licensing + { + prefix: "cc", + url: "http://creativecommons.org/ns#", + name: "CC - Creative Commons Vocabulary", + description: "Creative Commons licensing: permissions, prohibitions and attribution requirements", + ontologyUrl: "https://creativecommons.org/schema.rdf", + }, + + // Social web & discussions + { + prefix: "sioc", + url: "http://rdfs.org/sioc/ns#", + name: "SIOC - Semantically Interlinked Online Communities", + description: "Online communities: blog posts, forum threads, replies and user accounts", + ontologyUrl: "http://rdfs.org/sioc/ns", + }, + + // IoT / smart home + { + prefix: "saref", + url: "https://saref.etsi.org/core/", + name: "SAREF - Smart Appliances REFerence Ontology", + description: "Smart home and IoT: devices, functions, commands, measurements and states", + }, + + // Buildings & architecture + { + prefix: "bot", + url: "https://w3id.org/bot#", + name: "BOT - Building Topology Ontology", + description: "Buildings: sites, buildings, storeys, spaces, zones and construction elements", + ontologyUrl: "https://w3id.org/bot", + }, + + // Geospatial + { + prefix: "geo", + url: "http://www.opengis.net/ont/geosparql#", + name: "GeoSPARQL - OGC Geographic Query Language", + description: "Spatial features: geometries, WKT literals, topological relations and GIS queries", + ontologyUrl: "https://opengeospatial.github.io/ogc-geosparql/geosparql11/geo.ttl", + }, + + // Provenance extensions + { + prefix: "pav", + url: "http://purl.org/pav/", + name: "PAV - Provenance, Authoring and Versioning", + description: "Lightweight provenance: authors, curators, versions and source attribution", + }, + + // Open government data + { + prefix: "adms", + url: "http://www.w3.org/ns/adms#", + name: "ADMS - Asset Description Metadata Schema", + description: "Semantic assets and interoperability resources: specifications, schemas and vocabularies", + }, + { + prefix: "regorg", + url: "http://www.w3.org/ns/regorg#", + name: "RegOrg - Registered Organization Vocabulary", + description: "Legal entities and registered organizations: registration numbers and identifiers", + }, + + // Project metadata & vocabulary annotation + { + prefix: "doap", + url: "http://usefulinc.com/ns/doap#", + name: "DOAP - Description of a Project", + description: "Open-source projects: repositories, releases, maintainers and programming languages", + ontologyUrl: "https://raw.githubusercontent.com/ewilderj/doap/master/schema/doap.rdf", + }, + + // Dublin Core type vocabulary + { + prefix: "dcmitype", + url: "http://purl.org/dc/dcmitype/", + name: "DCMIType - DCMI Type Vocabulary", + description: "Content types: Dataset, Image, Software, Text, Sound, MovingImage and more", + }, ] as const; export const WELL_KNOWN_BY_PREFIX: Record< string, - { prefix: string; url: string; name: string; ontologyUrl?: string } + { prefix: string; url: string; name: string; description?: string; ontologyUrl?: string } > = Object.fromEntries(WELL_KNOWN_PREFIXES.map((p) => [p.prefix, p])) as any; /** From 5533623ec97f4f839957f76d8bb37e4fdb418ebd Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:03:50 +0200 Subject: [PATCH 15/22] fix(ontologies): vendor ical and sioc RDF into public/ to avoid CORS failures W3C ical.rdf and rdfs.org/sioc/ns do not send Access-Control-Allow-Origin headers, so browsers block the fetch. Vendor local copies into public/ontologies/ and point ontologyUrl to the app-relative path /ontologies/{file}.rdf. Network test updated to resolve /ontologies/ paths against public/ on disk instead of attempting a network fetch. --- public/ontologies/ical.rdf | 1687 +++++++++++++++++ public/ontologies/sioc.rdf | 921 +++++++++ .../wellKnownOntologies.network.test.ts | 40 +- src/utils/wellKnownOntologies.ts | 4 +- 4 files changed, 2638 insertions(+), 14 deletions(-) create mode 100644 public/ontologies/ical.rdf create mode 100644 public/ontologies/sioc.rdf diff --git a/public/ontologies/ical.rdf b/public/ontologies/ical.rdf new file mode 100644 index 00000000..f5a15975 --- /dev/null +++ b/public/ontologies/ical.rdf @@ -0,0 +1,1687 @@ + + + + + + + $Id: ical.rdf,v 1.14 2004/04/07 18:45:16 connolly Exp $ + subject to change with notice to www-rdf-calendar@w3.org + + + + + + + + + VEVENT + Provide a grouping of component properties that describe an event. + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + VTODO + Provide a grouping of calendar properties that describe a to-do. + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + VJOURNAL + Provide a grouping of component properties that describe a journal entry. + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + VFREEBUSY + Provide a grouping of component properties that describe either a request for free/busy time, describe a response to a request for free/busy time or describe a published set of busy time. + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + VTIMEZONE + Provide a grouping of component properties that defines a time zone. + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + VALARM + Provide a grouping of component properties that define an alarm. + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + CALSCALE + This property defines the calendar scale used for the calendar information specified in the iCalendar object. + + value type: TEXT + TEXT + + + + + + + METHOD + This property defines the iCalendar object method associated with the calendar object. + + value type: TEXT + TEXT + + + + + + + PRODID + This property specifies the identifier for the product that created the iCalendar object. + + value type: TEXT + TEXT + + + + + + + VERSION + This property specifies the identifier corresponding to the highest version number or the minimum and maximum range of the iCalendar specification that is required in order to interpret the iCalendar object. + + value type: TEXT + TEXT + + + + + + + ATTACH + The property provides the capability to associate a document object with a calendar component. + + default value type: URI + URI + + + + + + + + + + + + + + + + + + CATEGORIES + This property defines the categories for a calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + + + CLASS + This property defines the access classification for a calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + COMMENT + This property specifies non-processing information intended to provide a comment to the calendar user. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + + DESCRIPTION + This property provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + + + + + + GEO + This property specifies information related to the global position for the activity specified by a calendar component. + + value type: list of FLOAT + FLOAT + + + + + + + + + + + + + + + LOCATION + The property defines the intended venue for the activity defined by a calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + PERCENT-COMPLETE + This property is used by an assignee or delegatee of a to-do to convey the percent completion of a to-do to the Organizer. + + value type: INTEGER + INTEGER + + + + + + + + PRIORITY + The property defines the relative priority for a calendar component. + + value type: INTEGER + INTEGER + + + + + + + + + + + + + + + + + RESOURCES + This property defines the equipment or resources anticipated for an activity specified by a calendar entity.. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + STATUS + This property defines the overall status or confirmation for the calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + + + SUMMARY + This property defines a short summary or subject for the calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + + + + + COMPLETED + This property defines the date and time that a to-do was actually completed. + + value type: DATE-TIME + DATE-TIME + + + + + + + + DTEND + This property specifies the date and time that a calendar component ends. + + default value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + + + + + + + DUE + This property defines the date and time that a to-do is expected to be completed. + + default value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + DTSTART + This property specifies when the calendar component begins. + + default value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + + + + + + + + + + + + + DURATION + The property specifies a positive duration of time. + + value type: DURATION + DURATION + + + + + + + + + + + + + + + + + + + + + FREEBUSY + The property defines one or more free or busy time intervals. + + value type: PERIOD + PERIOD + + + + + + + + + + + + + + + TRANSP + This property defines whether an event is transparent or not to busy time searches. + + value type: TEXT + TEXT + + + + + + + + TZID + This property specifies the text value that uniquely identifies the "VTIMEZONE" calendar component. + + value type: TEXT + TEXT + + + + + + + + TZNAME + This property specifies the customary designation for a time zone description. + + value type: TEXT + TEXT + + + + + + + + TZOFFSETFROM + This property specifies the offset which is in use prior to this time zone observance. + + value type: UTC-OFFSET + UTC-OFFSET + + + + + + + + + + + + + + + + TZOFFSETTO + This property specifies the offset which is in use in this time zone observance. + + value type: UTC-OFFSET + UTC-OFFSET + + + + + + + + TZURL + The TZURL provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to- date version of itself. + + value type: URI + URI + + + + + + + + + + + + + ATTENDEE + The property defines an "Attendee" within a calendar component. + + value type: CAL-ADDRESS + CAL-ADDRESS + + + + + + + + + + + + + + + + + + + CONTACT + The property is used to represent contact information or alternately a reference to contact information associated with the calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + ORGANIZER + The property defines the organizer for a calendar component. + + value type: CAL-ADDRESS + CAL-ADDRESS + + + + + + + + + + + + + + + + + + RECURRENCE-ID + This property is used in conjunction with the "UID" and "SEQUENCE" property to identify a specific instance of a recurring "VEVENT", "VTODO" or "VJOURNAL" calendar component. The property value is the effective value of the "DTSTART" property of the recurrence instance. + + default value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + + RELATED-TO + The property is used to represent a relationship or reference between one calendar component and another. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + URL + This property defines a Uniform Resource Locator (URL) associated with the iCalendar object. + + value type: URI + URI + + + + + + + + + + + + + + UID + This property defines the persistent, globally unique identifier for the calendar component. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + + + + + + + + EXDATE + This property defines the list of date/time exceptions for a recurring calendar component. + + default value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + EXRULE + This property defines a rule or repeating pattern for an exception to a recurrence set. + + value type: RECUR + RECUR + + + + + + + + + + + + + + + + RDATE + This property defines the list of date/times for a recurrence set. + + default value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + + + + + + + + RRULE + This property defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions. + + value type: RECUR + RECUR + + + + + + + + + + + + + + + + + + + + ACTION + This property defines the action to be invoked when an alarm is triggered. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + REPEAT + This property defines the number of time the alarm should be repeated, after the initial trigger. + + value type: INTEGER + INTEGER + + + + + + + + TRIGGER + This property specifies when an alarm will trigger. + + default value type: DURATION + DURATION + + + + + + + + + + + + + + + + + + + + + + + + + CREATED + This property specifies the date and time that the calendar information was created by the calendar user agent in the calendar store. Note: This is analogous to the creation date and time for a file in the file system. + + value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + DTSTAMP + The property indicates the date/time that the instance of the iCalendar object was created. + + value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + + LAST-MODIFIED + The property specifies the date and time that the information associated with the calendar component was last revised in the calendar store. Note: This is analogous to the modification date and time for a file in the file system. + + value type: DATE-TIME + DATE-TIME + + + + + + + + + + + + + + + + + SEQUENCE + This property defines the revision sequence number of the calendar component within a sequence of revisions. + + value type: integer + integer + + + + + + + + + + + + + + + + Any property name with a "X-" prefix + This class of property provides a framework for defining non-standard properties. + + value type: TEXT + TEXT + + + + + + + REQUEST-STATUS + This property defines the status code returned for a scheduling request. + + value type: TEXT + TEXT + + + + + + + + + + + + + + + + diff --git a/public/ontologies/sioc.rdf b/public/ontologies/sioc.rdf new file mode 100644 index 00000000..78d061ab --- /dev/null +++ b/public/ontologies/sioc.rdf @@ -0,0 +1,921 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SIOC Core Ontology Namespace + Revision: 1.36 + SIOC (Semantically-Interlinked Online Communities) is an ontology for describing the information in online communities. +This information can be used to export information from online communities and to link them together. The scope of the application areas that SIOC can be used for includes (and is not limited to) weblogs, message boards, mailing lists and chat channels. + + + + + + Community + Community is a high-level concept that defines an online community and what it consists of. + + + + + + + + Container + An area in which content Items are contained. + + + + + + + + + Forum + A discussion area on which Posts or entries are made. + + + + + + Item + An Item is something which can be in a Container. + + + + + + + + + + Post + An article or message that can be posted to a Forum. + + + + + + + + Role + A Role is a function of a UserAccount within a scope of a particular Forum, Site, etc. + + + + + + + + + + Space + A Space is a place where data resides, e.g. on a website, desktop, fileshare, etc. + + + + + + + + + Site + A Site can be the location of an online community or set of communities, with UserAccounts and Usergroups creating Items in a set of Containers. It can be thought of as a web-accessible data Space. + + + + + + Thread + A container for a series of threaded discussion Posts or Items. + + + + + + User Account + A user account in an online community site. + + + + + + + + + + + Usergroup + A set of UserAccounts whose owners have a common purpose or interest. Can be used for access control purposes. + + + + + + + + + + + + about + Specifies that this Item is about a particular resource, e.g. a Post describing a book, hotel, etc. + + + + + + account of + Refers to the foaf:Agent or foaf:Person who owns this sioc:UserAccount. + + + + + + + + addressed to + Refers to who (e.g. a UserAccount, e-mail address, etc.) a particular Item is addressed to. + + + + + + + administrator of + + A Site that the UserAccount is an administrator of. + + + + + + + attachment + The URI of a file attached to an Item. + + + + + + avatar + An image or depiction used to represent this UserAccount. + + + + + + + container of + + An Item that this Container contains. + + + + + + + + content + The content of the Item in plain text format. + + + + + + + creator of + + A resource that the UserAccount is a creator of. + + + + + + delivered at + + When this was delivered, in ISO 8601 format. + + + + + + + discussion of + + The Item that this discussion is about. + + + + + + Links to a previous (older) revision of this Item or Post. + + + earlier version + + + + + + email + An electronic mail address of the UserAccount. + + + + + + email sha1 + An electronic mail address of the UserAccount, encoded using SHA1. + + + + + + + embeds knowledge + This links Items to embedded statements, facts and structured content. + + + + + + + feed + A feed (e.g. RSS, Atom, etc.) pertaining to this resource (e.g. for a Forum, Site, UserAccount, etc.). + + + + + follows + Indicates that one UserAccount follows another UserAccount (e.g. for microblog posts or other content item updates). + + + + + + + + function of + + A UserAccount that has this Role. + + + + + + generator + A URI for the application used to generate this Item. + + + + + + + has administrator + + A UserAccount that is an administrator of this Site. + + + + + + + has container + + The Container to which this Item belongs. + + + + + + + + has creator + + This is the UserAccount that made this resource. + + + + + + has discussion + + A discussion that is related to this Item. The discussion can be anything, for example, a sioc:Forum or sioc:Thread, a sioct:WikiArticle or simply a foaf:Document. + + + + + + + has function + + A Role that this UserAccount has. + + + + + + has host + + The Site that hosts this Container. + + + + + + + + has member + + A UserAccount that is a member of this Usergroup. + + + + + + + + has moderator + + A UserAccount that is a moderator of this Forum. + + + + + + + has modifier + + A UserAccount that modified this resource (e.g. Item, Container, Space). + + + + + + has owner + + A UserAccount that this resource is owned by. + + + + + + has parent + + A Container or Forum that this Container or Forum is a child of. + + + + + + + + has reply + + + Points to an Item or Post that is a reply or response to this Item or Post. + + + + + + + + has scope + + A resource that this Role applies to. + + + + + + has space + + A data Space which this resource is a part of. + + + + + + + has subscriber + + A UserAccount that is subscribed to this Container. + + + + + + + + has usergroup + + Points to a Usergroup that has certain access to this Space. + + + + + + + host of + + A Container that is hosted on this Site. + + + + + + + + id + An identifier of a SIOC concept instance. For example, a user ID. Must be unique for instances of each type of SIOC concept within the same site. + + + + + + ip address + The IP address used when creating this Item, UserAccount, etc. This can be associated with a creator. Some wiki articles list the IP addresses for the creator or modifiers when the usernames are absent. + + + + + + last activity date + The date and time of the last activity associated with a SIOC concept instance, and expressed in ISO 8601 format. This could be due to a reply Post or Comment, a modification to an Item, etc. + + + + + + + last item date + The date and time of the last Post (or Item) in a Forum (or a Container), in ISO 8601 format. + + + + + + + + last reply date + The date and time of the last reply Post or Comment, which could be associated with a starter Item or Post or with a Thread, and expressed in ISO 8601 format. + + + + + + + Links to a later (newer) revision of this Item or Post. + + + later version + + + + + + Links to the latest revision of this Item or Post. + + + latest version + + + + + likes + Used to indicate some form of endorsement by a UserAccount or Agent of an Item, Container, Space, UserAccount, etc. + + + + + link + A URI of a document which contains this SIOC object. + + + + + links to + Links extracted from hyperlinks within a SIOC concept, e.g. Post or Site. + + + + + + member of + + A Usergroup that this UserAccount is a member of. + + + + + + + mentions + Refers to a UserAccount that a particular Item mentions. + + + + + + + + moderator of + + A Forum that a UserAccount is a moderator of. + + + + + + + modifier of + + A resource that this UserAccount has modified. + + + + + + name + The name of a SIOC concept instance, e.g. a username for a UserAccount, group name for a Usergroup, etc. + + + + + + next by date + + Next Item or Post in a given Container sorted by date. + + + + + + + next version + + Links to the next revision of this Item or Post. + + + + + + + + note + A note associated with this resource, for example, if it has been edited by a UserAccount. + + + + + + num authors + The number of unique authors (UserAccounts and unregistered posters) who have contributed to this Item, Thread, Post, etc. + + + + + + num items + The number of Posts (or Items) in a Forum (or a Container). + + + + + + + num replies + The number of replies that this Item, Thread, Post, etc. has. Useful for when the reply structure is absent. + + + + + + num threads + The number of Threads (AKA discussion topics) in a Forum. + + + + + + + num views + The number of times this Item, Thread, UserAccount profile, etc. has been viewed. + + + + + + owner of + + A resource owned by a particular UserAccount, for example, a weblog or image gallery. + + + + + + parent of + + A child Container or Forum that this Container or Forum is a parent of. + + + + + + + + previous by date + + Previous Item or Post in a given Container sorted by date. + + + + + + + previous version + + Links to the previous revision of this Item or Post. + + + + + + + + read at + + When this was read, in ISO 8601 format. + + + + + + + related to + Related resources for this resource, e.g. for Posts, perhaps determined implicitly from topics or references. + + + + + reply of + + + Links to an Item or Post which this Item or Post is a reply to. + + + + + + + respond to + For the reply-to address set in email messages, IMs, etc. The property name was chosen to avoid confusion with has_reply/reply_of (the reply graph). + + + + + + + scope of + + A Role that has a scope of this resource. + + + + + + shared by + For shared Items where there is a certain creator_of and an intermediary who shares or forwards it (e.g. as a sibling Item). + + + + + + + + sibling + An Item may have a sibling or a twin that exists in a different Container, but the siblings may differ in some small way (for example, language, category, etc.). The sibling of this Item should be self-describing (that is, it should contain all available information). + + + + + + + space of + + A resource which belongs to this data Space. + + + + + + + subscriber of + + A Container that a UserAccount is subscribed to. + + + + + + + + topic + A topic of interest, linking to the appropriate URI, e.g. in the Open Directory Project or of a SKOS category. + + + + + + usergroup of + + A Space that the Usergroup has access to. + + + + + + + + + User + UserAccount is now preferred. This is a deprecated class for a User in an online community site. + + + + + + + + + This class is deprecated. Use sioc:UserAccount from the SIOC ontology instead. + + + + title + + This is the title (subject line) of the Post. Note that for a Post within a threaded discussion that has no parents, it would detail the topic thread. + + + + This property is deprecated. Use dcterms:title from the Dublin Core ontology instead. + + + + content encoded + + The encoded content of the Post, contained in CDATA areas. + + + + This property is deprecated. Use content:encoded from the RSS 1.0 content module instead. + + + + created at + + When this was created, in ISO 8601 format. + + + + This property is deprecated. Use dcterms:created from the Dublin Core ontology instead. + + + + description + + The content of the Post. + + + + This property is deprecated. Use sioc:content or other methods (AtomOwl, content:encoded from RSS 1.0, etc.) instead. + + + + first name + + First (real) name of this User. Synonyms include given name or christian name. + + + + This property is deprecated. Use foaf:name or foaf:firstName from the FOAF vocabulary instead. + + + + group of + + + This property has been renamed. Use sioc:usergroup_of instead. + + + + has group + + + This property has been renamed. Use sioc:has_usergroup instead. + + + + has part + + + An resource that is a part of this subject. + + This property is deprecated. Use dcterms:hasPart from the Dublin Core ontology instead. + + + + last name + + Last (real) name of this user. Synonyms include surname or family name. + + + + This property is deprecated. Use foaf:name or foaf:surname from the FOAF vocabulary instead. + + + + modified at + + When this was modified, in ISO 8601 format. + + + + This property is deprecated. Use dcterms:modified from the Dublin Core ontology instead. + + + + part of + + + A resource that the subject is a part of. + + This property is deprecated. Use dcterms:isPartOf from the Dublin Core ontology instead. + + + + reference + + Links either created explicitly or extracted implicitly on the HTML level from the Post. + + + Renamed to sioc:links_to. + + + + subject + + Keyword(s) describing subject of the Post. + + + + This property is deprecated. Use dcterms:subject from the Dublin Core ontology for text keywords and sioc:topic if the subject can be represented by a URI instead. + + + diff --git a/src/__tests__/stores/wellKnownOntologies.network.test.ts b/src/__tests__/stores/wellKnownOntologies.network.test.ts index 4faef8be..41e2e967 100644 --- a/src/__tests__/stores/wellKnownOntologies.network.test.ts +++ b/src/__tests__/stores/wellKnownOntologies.network.test.ts @@ -1,21 +1,38 @@ // @vitest-environment node import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { Readable } from "node:stream"; import { Parser as N3Parser } from "n3"; import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from "../../utils/wellKnownOntologies"; -async function fetchAndParse(url: string): Promise { - const resp = await fetch(url, { - headers: { Accept: "text/turtle, application/rdf+xml, application/n-triples, text/n3, */*;q=0.1" }, - redirect: "follow", - }); - - if (!resp.ok) { - throw new Error(`HTTP ${resp.status} from ${url}`); +// Paths starting with /ontologies/ are vendored files in public/ — read from disk in Node. +function toAbsoluteUrl(url: string): { url: string; isFile: boolean } { + if (url.startsWith("/ontologies/")) { + const abs = resolve(__dirname, "../../../public", url.slice(1)); + return { url: abs, isFile: true }; } + return { url, isFile: false }; +} + +async function fetchAndParse(rawUrl: string): Promise { + const { url, isFile } = toAbsoluteUrl(rawUrl); - const body = await resp.text(); - const contentType = (resp.headers.get("content-type") || "").toLowerCase(); + let body: string; + let contentType: string; + + if (isFile) { + body = readFileSync(url, "utf-8"); + contentType = url.endsWith(".rdf") ? "application/rdf+xml" : "text/turtle"; + } else { + const resp = await fetch(url, { + headers: { Accept: "text/turtle, application/rdf+xml, application/n-triples, text/n3, */*;q=0.1" }, + redirect: "follow", + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${url}`); + body = await resp.text(); + contentType = (resp.headers.get("content-type") || "").toLowerCase(); + } const isRdfXml = contentType.includes("application/rdf+xml") || @@ -34,8 +51,7 @@ async function fetchAndParse(url: string): Promise { parser.on("data", () => { count++; }); parser.on("end", () => resolve(count)); parser.on("error", (err: Error) => reject(new Error(`RDF/XML parse error from ${url}: ${err.message}`))); - const readable = Readable.from([body]); - readable.pipe(parser); + Readable.from([body]).pipe(parser); }); } diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index 125fd51e..fad45fca 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -272,7 +272,7 @@ export const WELL_KNOWN_PREFIXES = [ url: "http://www.w3.org/2002/12/cal/ical#", name: "iCal - iCalendar Vocabulary", description: "Calendar events: meetings, recurring schedules, alarms and calendar components", - ontologyUrl: "http://www.w3.org/2002/12/cal/ical.rdf", + ontologyUrl: "/ontologies/ical.rdf", }, // Bibliographic & scholarly @@ -344,7 +344,7 @@ export const WELL_KNOWN_PREFIXES = [ url: "http://rdfs.org/sioc/ns#", name: "SIOC - Semantically Interlinked Online Communities", description: "Online communities: blog posts, forum threads, replies and user accounts", - ontologyUrl: "http://rdfs.org/sioc/ns", + ontologyUrl: "/ontologies/sioc.rdf", }, // IoT / smart home From 409209233fed1e1ad2f6de33a9d081febd95fa74 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:04:50 +0200 Subject: [PATCH 16/22] Revert "fix(ontologies): vendor ical and sioc RDF into public/ to avoid CORS failures" This reverts commit 5533623ec97f4f839957f76d8bb37e4fdb418ebd. --- public/ontologies/ical.rdf | 1687 ----------------- public/ontologies/sioc.rdf | 921 --------- .../wellKnownOntologies.network.test.ts | 40 +- src/utils/wellKnownOntologies.ts | 4 +- 4 files changed, 14 insertions(+), 2638 deletions(-) delete mode 100644 public/ontologies/ical.rdf delete mode 100644 public/ontologies/sioc.rdf diff --git a/public/ontologies/ical.rdf b/public/ontologies/ical.rdf deleted file mode 100644 index f5a15975..00000000 --- a/public/ontologies/ical.rdf +++ /dev/null @@ -1,1687 +0,0 @@ - - - - - - - $Id: ical.rdf,v 1.14 2004/04/07 18:45:16 connolly Exp $ - subject to change with notice to www-rdf-calendar@w3.org - - - - - - - - - VEVENT - Provide a grouping of component properties that describe an event. - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - VTODO - Provide a grouping of calendar properties that describe a to-do. - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - VJOURNAL - Provide a grouping of component properties that describe a journal entry. - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - VFREEBUSY - Provide a grouping of component properties that describe either a request for free/busy time, describe a response to a request for free/busy time or describe a published set of busy time. - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - VTIMEZONE - Provide a grouping of component properties that defines a time zone. - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - VALARM - Provide a grouping of component properties that define an alarm. - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - - 0 - - - - - CALSCALE - This property defines the calendar scale used for the calendar information specified in the iCalendar object. - - value type: TEXT - TEXT - - - - - - - METHOD - This property defines the iCalendar object method associated with the calendar object. - - value type: TEXT - TEXT - - - - - - - PRODID - This property specifies the identifier for the product that created the iCalendar object. - - value type: TEXT - TEXT - - - - - - - VERSION - This property specifies the identifier corresponding to the highest version number or the minimum and maximum range of the iCalendar specification that is required in order to interpret the iCalendar object. - - value type: TEXT - TEXT - - - - - - - ATTACH - The property provides the capability to associate a document object with a calendar component. - - default value type: URI - URI - - - - - - - - - - - - - - - - - - CATEGORIES - This property defines the categories for a calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - - - CLASS - This property defines the access classification for a calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - COMMENT - This property specifies non-processing information intended to provide a comment to the calendar user. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - - DESCRIPTION - This property provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - - - - - - GEO - This property specifies information related to the global position for the activity specified by a calendar component. - - value type: list of FLOAT - FLOAT - - - - - - - - - - - - - - - LOCATION - The property defines the intended venue for the activity defined by a calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - PERCENT-COMPLETE - This property is used by an assignee or delegatee of a to-do to convey the percent completion of a to-do to the Organizer. - - value type: INTEGER - INTEGER - - - - - - - - PRIORITY - The property defines the relative priority for a calendar component. - - value type: INTEGER - INTEGER - - - - - - - - - - - - - - - - - RESOURCES - This property defines the equipment or resources anticipated for an activity specified by a calendar entity.. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - STATUS - This property defines the overall status or confirmation for the calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - - - SUMMARY - This property defines a short summary or subject for the calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - - - - - COMPLETED - This property defines the date and time that a to-do was actually completed. - - value type: DATE-TIME - DATE-TIME - - - - - - - - DTEND - This property specifies the date and time that a calendar component ends. - - default value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - - - - - - - DUE - This property defines the date and time that a to-do is expected to be completed. - - default value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - DTSTART - This property specifies when the calendar component begins. - - default value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - - - - - - - - - - - - - DURATION - The property specifies a positive duration of time. - - value type: DURATION - DURATION - - - - - - - - - - - - - - - - - - - - - FREEBUSY - The property defines one or more free or busy time intervals. - - value type: PERIOD - PERIOD - - - - - - - - - - - - - - - TRANSP - This property defines whether an event is transparent or not to busy time searches. - - value type: TEXT - TEXT - - - - - - - - TZID - This property specifies the text value that uniquely identifies the "VTIMEZONE" calendar component. - - value type: TEXT - TEXT - - - - - - - - TZNAME - This property specifies the customary designation for a time zone description. - - value type: TEXT - TEXT - - - - - - - - TZOFFSETFROM - This property specifies the offset which is in use prior to this time zone observance. - - value type: UTC-OFFSET - UTC-OFFSET - - - - - - - - - - - - - - - - TZOFFSETTO - This property specifies the offset which is in use in this time zone observance. - - value type: UTC-OFFSET - UTC-OFFSET - - - - - - - - TZURL - The TZURL provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to- date version of itself. - - value type: URI - URI - - - - - - - - - - - - - ATTENDEE - The property defines an "Attendee" within a calendar component. - - value type: CAL-ADDRESS - CAL-ADDRESS - - - - - - - - - - - - - - - - - - - CONTACT - The property is used to represent contact information or alternately a reference to contact information associated with the calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - ORGANIZER - The property defines the organizer for a calendar component. - - value type: CAL-ADDRESS - CAL-ADDRESS - - - - - - - - - - - - - - - - - - RECURRENCE-ID - This property is used in conjunction with the "UID" and "SEQUENCE" property to identify a specific instance of a recurring "VEVENT", "VTODO" or "VJOURNAL" calendar component. The property value is the effective value of the "DTSTART" property of the recurrence instance. - - default value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - - RELATED-TO - The property is used to represent a relationship or reference between one calendar component and another. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - URL - This property defines a Uniform Resource Locator (URL) associated with the iCalendar object. - - value type: URI - URI - - - - - - - - - - - - - - UID - This property defines the persistent, globally unique identifier for the calendar component. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - - - - - - - - EXDATE - This property defines the list of date/time exceptions for a recurring calendar component. - - default value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - EXRULE - This property defines a rule or repeating pattern for an exception to a recurrence set. - - value type: RECUR - RECUR - - - - - - - - - - - - - - - - RDATE - This property defines the list of date/times for a recurrence set. - - default value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - - - - - - - - RRULE - This property defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions. - - value type: RECUR - RECUR - - - - - - - - - - - - - - - - - - - - ACTION - This property defines the action to be invoked when an alarm is triggered. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - REPEAT - This property defines the number of time the alarm should be repeated, after the initial trigger. - - value type: INTEGER - INTEGER - - - - - - - - TRIGGER - This property specifies when an alarm will trigger. - - default value type: DURATION - DURATION - - - - - - - - - - - - - - - - - - - - - - - - - CREATED - This property specifies the date and time that the calendar information was created by the calendar user agent in the calendar store. Note: This is analogous to the creation date and time for a file in the file system. - - value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - DTSTAMP - The property indicates the date/time that the instance of the iCalendar object was created. - - value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - - LAST-MODIFIED - The property specifies the date and time that the information associated with the calendar component was last revised in the calendar store. Note: This is analogous to the modification date and time for a file in the file system. - - value type: DATE-TIME - DATE-TIME - - - - - - - - - - - - - - - - - SEQUENCE - This property defines the revision sequence number of the calendar component within a sequence of revisions. - - value type: integer - integer - - - - - - - - - - - - - - - - Any property name with a "X-" prefix - This class of property provides a framework for defining non-standard properties. - - value type: TEXT - TEXT - - - - - - - REQUEST-STATUS - This property defines the status code returned for a scheduling request. - - value type: TEXT - TEXT - - - - - - - - - - - - - - - - diff --git a/public/ontologies/sioc.rdf b/public/ontologies/sioc.rdf deleted file mode 100644 index 78d061ab..00000000 --- a/public/ontologies/sioc.rdf +++ /dev/null @@ -1,921 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SIOC Core Ontology Namespace - Revision: 1.36 - SIOC (Semantically-Interlinked Online Communities) is an ontology for describing the information in online communities. -This information can be used to export information from online communities and to link them together. The scope of the application areas that SIOC can be used for includes (and is not limited to) weblogs, message boards, mailing lists and chat channels. - - - - - - Community - Community is a high-level concept that defines an online community and what it consists of. - - - - - - - - Container - An area in which content Items are contained. - - - - - - - - - Forum - A discussion area on which Posts or entries are made. - - - - - - Item - An Item is something which can be in a Container. - - - - - - - - - - Post - An article or message that can be posted to a Forum. - - - - - - - - Role - A Role is a function of a UserAccount within a scope of a particular Forum, Site, etc. - - - - - - - - - - Space - A Space is a place where data resides, e.g. on a website, desktop, fileshare, etc. - - - - - - - - - Site - A Site can be the location of an online community or set of communities, with UserAccounts and Usergroups creating Items in a set of Containers. It can be thought of as a web-accessible data Space. - - - - - - Thread - A container for a series of threaded discussion Posts or Items. - - - - - - User Account - A user account in an online community site. - - - - - - - - - - - Usergroup - A set of UserAccounts whose owners have a common purpose or interest. Can be used for access control purposes. - - - - - - - - - - - - about - Specifies that this Item is about a particular resource, e.g. a Post describing a book, hotel, etc. - - - - - - account of - Refers to the foaf:Agent or foaf:Person who owns this sioc:UserAccount. - - - - - - - - addressed to - Refers to who (e.g. a UserAccount, e-mail address, etc.) a particular Item is addressed to. - - - - - - - administrator of - - A Site that the UserAccount is an administrator of. - - - - - - - attachment - The URI of a file attached to an Item. - - - - - - avatar - An image or depiction used to represent this UserAccount. - - - - - - - container of - - An Item that this Container contains. - - - - - - - - content - The content of the Item in plain text format. - - - - - - - creator of - - A resource that the UserAccount is a creator of. - - - - - - delivered at - - When this was delivered, in ISO 8601 format. - - - - - - - discussion of - - The Item that this discussion is about. - - - - - - Links to a previous (older) revision of this Item or Post. - - - earlier version - - - - - - email - An electronic mail address of the UserAccount. - - - - - - email sha1 - An electronic mail address of the UserAccount, encoded using SHA1. - - - - - - - embeds knowledge - This links Items to embedded statements, facts and structured content. - - - - - - - feed - A feed (e.g. RSS, Atom, etc.) pertaining to this resource (e.g. for a Forum, Site, UserAccount, etc.). - - - - - follows - Indicates that one UserAccount follows another UserAccount (e.g. for microblog posts or other content item updates). - - - - - - - - function of - - A UserAccount that has this Role. - - - - - - generator - A URI for the application used to generate this Item. - - - - - - - has administrator - - A UserAccount that is an administrator of this Site. - - - - - - - has container - - The Container to which this Item belongs. - - - - - - - - has creator - - This is the UserAccount that made this resource. - - - - - - has discussion - - A discussion that is related to this Item. The discussion can be anything, for example, a sioc:Forum or sioc:Thread, a sioct:WikiArticle or simply a foaf:Document. - - - - - - - has function - - A Role that this UserAccount has. - - - - - - has host - - The Site that hosts this Container. - - - - - - - - has member - - A UserAccount that is a member of this Usergroup. - - - - - - - - has moderator - - A UserAccount that is a moderator of this Forum. - - - - - - - has modifier - - A UserAccount that modified this resource (e.g. Item, Container, Space). - - - - - - has owner - - A UserAccount that this resource is owned by. - - - - - - has parent - - A Container or Forum that this Container or Forum is a child of. - - - - - - - - has reply - - - Points to an Item or Post that is a reply or response to this Item or Post. - - - - - - - - has scope - - A resource that this Role applies to. - - - - - - has space - - A data Space which this resource is a part of. - - - - - - - has subscriber - - A UserAccount that is subscribed to this Container. - - - - - - - - has usergroup - - Points to a Usergroup that has certain access to this Space. - - - - - - - host of - - A Container that is hosted on this Site. - - - - - - - - id - An identifier of a SIOC concept instance. For example, a user ID. Must be unique for instances of each type of SIOC concept within the same site. - - - - - - ip address - The IP address used when creating this Item, UserAccount, etc. This can be associated with a creator. Some wiki articles list the IP addresses for the creator or modifiers when the usernames are absent. - - - - - - last activity date - The date and time of the last activity associated with a SIOC concept instance, and expressed in ISO 8601 format. This could be due to a reply Post or Comment, a modification to an Item, etc. - - - - - - - last item date - The date and time of the last Post (or Item) in a Forum (or a Container), in ISO 8601 format. - - - - - - - - last reply date - The date and time of the last reply Post or Comment, which could be associated with a starter Item or Post or with a Thread, and expressed in ISO 8601 format. - - - - - - - Links to a later (newer) revision of this Item or Post. - - - later version - - - - - - Links to the latest revision of this Item or Post. - - - latest version - - - - - likes - Used to indicate some form of endorsement by a UserAccount or Agent of an Item, Container, Space, UserAccount, etc. - - - - - link - A URI of a document which contains this SIOC object. - - - - - links to - Links extracted from hyperlinks within a SIOC concept, e.g. Post or Site. - - - - - - member of - - A Usergroup that this UserAccount is a member of. - - - - - - - mentions - Refers to a UserAccount that a particular Item mentions. - - - - - - - - moderator of - - A Forum that a UserAccount is a moderator of. - - - - - - - modifier of - - A resource that this UserAccount has modified. - - - - - - name - The name of a SIOC concept instance, e.g. a username for a UserAccount, group name for a Usergroup, etc. - - - - - - next by date - - Next Item or Post in a given Container sorted by date. - - - - - - - next version - - Links to the next revision of this Item or Post. - - - - - - - - note - A note associated with this resource, for example, if it has been edited by a UserAccount. - - - - - - num authors - The number of unique authors (UserAccounts and unregistered posters) who have contributed to this Item, Thread, Post, etc. - - - - - - num items - The number of Posts (or Items) in a Forum (or a Container). - - - - - - - num replies - The number of replies that this Item, Thread, Post, etc. has. Useful for when the reply structure is absent. - - - - - - num threads - The number of Threads (AKA discussion topics) in a Forum. - - - - - - - num views - The number of times this Item, Thread, UserAccount profile, etc. has been viewed. - - - - - - owner of - - A resource owned by a particular UserAccount, for example, a weblog or image gallery. - - - - - - parent of - - A child Container or Forum that this Container or Forum is a parent of. - - - - - - - - previous by date - - Previous Item or Post in a given Container sorted by date. - - - - - - - previous version - - Links to the previous revision of this Item or Post. - - - - - - - - read at - - When this was read, in ISO 8601 format. - - - - - - - related to - Related resources for this resource, e.g. for Posts, perhaps determined implicitly from topics or references. - - - - - reply of - - - Links to an Item or Post which this Item or Post is a reply to. - - - - - - - respond to - For the reply-to address set in email messages, IMs, etc. The property name was chosen to avoid confusion with has_reply/reply_of (the reply graph). - - - - - - - scope of - - A Role that has a scope of this resource. - - - - - - shared by - For shared Items where there is a certain creator_of and an intermediary who shares or forwards it (e.g. as a sibling Item). - - - - - - - - sibling - An Item may have a sibling or a twin that exists in a different Container, but the siblings may differ in some small way (for example, language, category, etc.). The sibling of this Item should be self-describing (that is, it should contain all available information). - - - - - - - space of - - A resource which belongs to this data Space. - - - - - - - subscriber of - - A Container that a UserAccount is subscribed to. - - - - - - - - topic - A topic of interest, linking to the appropriate URI, e.g. in the Open Directory Project or of a SKOS category. - - - - - - usergroup of - - A Space that the Usergroup has access to. - - - - - - - - - User - UserAccount is now preferred. This is a deprecated class for a User in an online community site. - - - - - - - - - This class is deprecated. Use sioc:UserAccount from the SIOC ontology instead. - - - - title - - This is the title (subject line) of the Post. Note that for a Post within a threaded discussion that has no parents, it would detail the topic thread. - - - - This property is deprecated. Use dcterms:title from the Dublin Core ontology instead. - - - - content encoded - - The encoded content of the Post, contained in CDATA areas. - - - - This property is deprecated. Use content:encoded from the RSS 1.0 content module instead. - - - - created at - - When this was created, in ISO 8601 format. - - - - This property is deprecated. Use dcterms:created from the Dublin Core ontology instead. - - - - description - - The content of the Post. - - - - This property is deprecated. Use sioc:content or other methods (AtomOwl, content:encoded from RSS 1.0, etc.) instead. - - - - first name - - First (real) name of this User. Synonyms include given name or christian name. - - - - This property is deprecated. Use foaf:name or foaf:firstName from the FOAF vocabulary instead. - - - - group of - - - This property has been renamed. Use sioc:usergroup_of instead. - - - - has group - - - This property has been renamed. Use sioc:has_usergroup instead. - - - - has part - - - An resource that is a part of this subject. - - This property is deprecated. Use dcterms:hasPart from the Dublin Core ontology instead. - - - - last name - - Last (real) name of this user. Synonyms include surname or family name. - - - - This property is deprecated. Use foaf:name or foaf:surname from the FOAF vocabulary instead. - - - - modified at - - When this was modified, in ISO 8601 format. - - - - This property is deprecated. Use dcterms:modified from the Dublin Core ontology instead. - - - - part of - - - A resource that the subject is a part of. - - This property is deprecated. Use dcterms:isPartOf from the Dublin Core ontology instead. - - - - reference - - Links either created explicitly or extracted implicitly on the HTML level from the Post. - - - Renamed to sioc:links_to. - - - - subject - - Keyword(s) describing subject of the Post. - - - - This property is deprecated. Use dcterms:subject from the Dublin Core ontology for text keywords and sioc:topic if the subject can be represented by a URI instead. - - - diff --git a/src/__tests__/stores/wellKnownOntologies.network.test.ts b/src/__tests__/stores/wellKnownOntologies.network.test.ts index 41e2e967..4faef8be 100644 --- a/src/__tests__/stores/wellKnownOntologies.network.test.ts +++ b/src/__tests__/stores/wellKnownOntologies.network.test.ts @@ -1,39 +1,22 @@ // @vitest-environment node import { describe, it, expect } from "vitest"; -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; import { Readable } from "node:stream"; import { Parser as N3Parser } from "n3"; import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from "../../utils/wellKnownOntologies"; -// Paths starting with /ontologies/ are vendored files in public/ — read from disk in Node. -function toAbsoluteUrl(url: string): { url: string; isFile: boolean } { - if (url.startsWith("/ontologies/")) { - const abs = resolve(__dirname, "../../../public", url.slice(1)); - return { url: abs, isFile: true }; - } - return { url, isFile: false }; -} - -async function fetchAndParse(rawUrl: string): Promise { - const { url, isFile } = toAbsoluteUrl(rawUrl); +async function fetchAndParse(url: string): Promise { + const resp = await fetch(url, { + headers: { Accept: "text/turtle, application/rdf+xml, application/n-triples, text/n3, */*;q=0.1" }, + redirect: "follow", + }); - let body: string; - let contentType: string; - - if (isFile) { - body = readFileSync(url, "utf-8"); - contentType = url.endsWith(".rdf") ? "application/rdf+xml" : "text/turtle"; - } else { - const resp = await fetch(url, { - headers: { Accept: "text/turtle, application/rdf+xml, application/n-triples, text/n3, */*;q=0.1" }, - redirect: "follow", - }); - if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${url}`); - body = await resp.text(); - contentType = (resp.headers.get("content-type") || "").toLowerCase(); + if (!resp.ok) { + throw new Error(`HTTP ${resp.status} from ${url}`); } + const body = await resp.text(); + const contentType = (resp.headers.get("content-type") || "").toLowerCase(); + const isRdfXml = contentType.includes("application/rdf+xml") || contentType.includes("text/xml") || @@ -51,7 +34,8 @@ async function fetchAndParse(rawUrl: string): Promise { parser.on("data", () => { count++; }); parser.on("end", () => resolve(count)); parser.on("error", (err: Error) => reject(new Error(`RDF/XML parse error from ${url}: ${err.message}`))); - Readable.from([body]).pipe(parser); + const readable = Readable.from([body]); + readable.pipe(parser); }); } diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index fad45fca..125fd51e 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -272,7 +272,7 @@ export const WELL_KNOWN_PREFIXES = [ url: "http://www.w3.org/2002/12/cal/ical#", name: "iCal - iCalendar Vocabulary", description: "Calendar events: meetings, recurring schedules, alarms and calendar components", - ontologyUrl: "/ontologies/ical.rdf", + ontologyUrl: "http://www.w3.org/2002/12/cal/ical.rdf", }, // Bibliographic & scholarly @@ -344,7 +344,7 @@ export const WELL_KNOWN_PREFIXES = [ url: "http://rdfs.org/sioc/ns#", name: "SIOC - Semantically Interlinked Online Communities", description: "Online communities: blog posts, forum threads, replies and user accounts", - ontologyUrl: "/ontologies/sioc.rdf", + ontologyUrl: "http://rdfs.org/sioc/ns", }, // IoT / smart home From 9e56b68c92fad58dc94fdb602da5797f69ef2e80 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:11:26 +0200 Subject: [PATCH 17/22] feat(mcp): add searchOntologies tool + wire discovery workflow into agent guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tool: searchOntologies(query) — filters the well-known registry by keyword or use-case phrase against prefix, name, and description. Returns prefix/name/ description/namespace/loadUrl so the AI knows exactly what to call next. loadOntology: made url required (empty call now returns a helpful error pointing to searchOntologies); error suggestions now search descriptions too and return richer objects. mcpServerDescription + help: add ONTOLOGY DISCOVERY section explaining that OWL/RDFS/RDF/XSD are always pre-loaded and that searchOntologies → loadOntology is the mandatory first step before authoring any domain model. Recommended workflow updated to start with discovery. --- src/mcp/manifest.ts | 26 +++++++++++++-- src/mcp/tools/graph.ts | 73 +++++++++++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/mcp/manifest.ts b/src/mcp/manifest.ts index e44f7a76..e187d132 100644 --- a/src/mcp/manifest.ts +++ b/src/mcp/manifest.ts @@ -12,10 +12,29 @@ export const mcpServerDescription = ' • "Punned" resources (typed as both, e.g. owl:Class AND owl:NamedIndividual) appear in both views.\n' + 'Switching views replaces the entire canvas — ABox nodes are invisible in TBox view and vice versa. Use setViewMode before exportImage to capture the right layer. addNode writes triples to the store and the canvas populates automatically in the correct view.\n\n' + 'Architecture for AI agents: The app has two coupled layers. (1) N3 RDF store (urn:vg:data) — source of truth for all triples. addNode/addLink write here first. (2) Reactodia canvas — mirrors the store subset matching the active view as draggable node cards and arrows. Nodes start collapsed; call expandNode or expandAll to reveal annotation property cards. OWL-RL reasoning writes inferred triples back to the store (urn:vg:inferred) and refreshes the canvas.\n\n' + - 'Recommended workflow: setViewMode("tbox") → loadOntology → addNode ×N (owl:Class etc.) → addLink ×N (subClassOf etc.) → runLayout → setViewMode("abox") → addNode ×N (individuals) → addLink ×N → runLayout → runReasoning → fitCanvas → exportImage(svg).\n\n' + + 'ONTOLOGY DISCOVERY — always the first step:\n' + + 'OWL, RDFS, RDF, and XSD are pre-loaded. All other ontologies must be loaded explicitly:\n' + + ' 1. searchOntologies("use case") — find the right prefix ("calendar" → ical, "music" → mo, "building" → bot, "e-commerce" → gr, …)\n' + + ' 2. loadOntology("") — load into TBox. Repeat for each domain.\n' + + ' 3. Register a namespace prefix: addNamespace(prefix, namespace) if you want short-form IRIs.\n\n' + + 'Recommended workflow: searchOntologies → loadOntology ×N → addNamespace ×N → setViewMode("tbox") → addNode ×N (owl:Class etc.) → addLink ×N (subClassOf etc.) → runLayout → setViewMode("abox") → addNode ×N (individuals) → addLink ×N → runLayout → runReasoning → fitCanvas → exportImage(svg).\n\n' + 'Agent integration: (1) Claude Code / Playwright — call window.__mcpTools[name](params) via browser_evaluate. (2) AI Relay Bridge — any AI chat (ChatGPT, Claude.ai, Gemini) can control Ontosphere via a bookmarklet relay that intercepts JSON-RPC 2.0 tool calls and injects results back automatically; see docs/relay-bridge.md. Full agent guide: AGENTS.md. Example sessions with SVG snapshots: docs/mcp-demo/.'; export const mcpManifest: McpToolManifestEntry[] = [ + { + name: 'searchOntologies', + description: + 'Search the well-known ontology registry by keyword or use-case phrase. Returns prefix, name, description, namespace URI, and load URL for each match. ' + + 'Call this FIRST to discover which ontologies cover your domain — e.g. "calendar" → ical, "music" → mo, "building" → bot, "spatial" → geo, "e-commerce" → gr, "citation" → cito. ' + + 'OWL/RDFS/RDF/XSD are always pre-loaded and do not appear as actionable results. ' + + 'Leave query empty to list all ~55 registered ontologies.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Keyword or use-case phrase. Leave empty for full listing.' }, + }, + }, + }, { name: 'loadRdf', description: @@ -37,7 +56,10 @@ export const mcpManifest: McpToolManifestEntry[] = [ }, { name: 'loadOntology', - description: 'Load TBox ontology for type hints and reasoning support. Does NOT add canvas nodes. Use for schema/class definitions. To load instance data as canvas nodes, use loadRdf instead.', + description: + 'Load a TBox ontology by prefix name, namespace URI, or direct file URL. Does NOT add canvas nodes — use loadRdf for instance data. ' + + 'Call searchOntologies first to find the right prefix for your use case. ' + + 'Examples: loadOntology("ical") for calendar events, loadOntology("mo") for music, loadOntology("bot") for buildings, loadOntology("gr") for e-commerce.', inputSchema: { type: 'object', properties: { diff --git a/src/mcp/tools/graph.ts b/src/mcp/tools/graph.ts index 5187b6ad..0ebba1dd 100644 --- a/src/mcp/tools/graph.ts +++ b/src/mcp/tools/graph.ts @@ -82,35 +82,72 @@ const loadRdf: McpTool = { }, }; +// --------------------------------------------------------------------------- +// searchOntologies +// --------------------------------------------------------------------------- +const searchOntologies: McpTool = { + name: 'searchOntologies', + description: 'Search the well-known ontology registry by keyword or use case. Returns matching ontologies with prefix, name, description, namespace URI, and load URL. Call this to discover which ontologies to load before calling loadOntology.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Keyword or use-case phrase (e.g. "calendar", "music", "building", "e-commerce", "citation", "spatial", "IoT"). Leave empty to list all registered ontologies.', + }, + }, + }, + async handler(params): Promise { + const { query = '' } = (params ?? {}) as { query?: string }; + const q = query.trim().toLowerCase(); + const matches = q + ? WELL_KNOWN_PREFIXES.filter(e => + e.prefix.toLowerCase().includes(q) || + e.name.toLowerCase().includes(q) || + ((e as any).description as string | undefined)?.toLowerCase().includes(q) + ) + : [...WELL_KNOWN_PREFIXES]; + + const ontologies = matches.map(e => ({ + prefix: e.prefix, + name: e.name, + description: (e as any).description ?? '', + namespace: e.url, + loadUrl: resolveOntologyLoadUrl(e.prefix), + })); + + return { + success: true, + data: { query: query || '(all)', count: ontologies.length, ontologies }, + }; + }, +}; + // --------------------------------------------------------------------------- // loadOntology // --------------------------------------------------------------------------- const loadOntology: McpTool = { name: 'loadOntology', description: - 'Load a well-known ontology by prefix name (e.g. "bfo", "ro", "iao", "foaf", "pmdco"), ' + + 'Load a well-known ontology by prefix name (e.g. "bibo", "ro", "foaf", "saref"), ' + 'by its namespace URL, or by any direct ontology file URL. ' + - 'Call with url="" or omit url to list all available well-known ontologies.', + 'Use searchOntologies first to discover the right prefix for your use case.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: - 'Prefix name (e.g. "bfo"), namespace IRI, or direct ontology URL. ' + - 'Leave empty to list available well-known ontologies.', + 'Prefix name (e.g. "bibo"), namespace IRI, or direct ontology URL.', }, }, + required: ['url'], }, async handler(params): Promise { const { url = '' } = (params ?? {}) as { url?: string }; - // Empty call — return registry listing if (!url.trim()) { - const known = WELL_KNOWN_PREFIXES - .filter(p => (p as any).ontologyUrl || p.url) - .map(p => ({ prefix: p.prefix, name: p.name, namespace: p.url, ontologyUrl: (p as any).ontologyUrl ?? p.url })); - return { success: true, data: { availableOntologies: known } }; + return { success: false, error: 'url is required. Use searchOntologies to discover available ontologies.' }; } const resolvedUrl = resolveOntologyLoadUrl(url); @@ -119,14 +156,18 @@ const loadOntology: McpTool = { await rdfManager.loadRDFFromUrl(resolvedUrl, { corsProxyUrl }); return { success: true, data: { loaded: resolvedUrl, requestedAs: url !== resolvedUrl ? url : undefined } }; } catch (e) { - // Suggest close matches from the registry const q = url.toLowerCase(); const suggestions = WELL_KNOWN_PREFIXES - .filter(p => p.prefix.includes(q) || p.name.toLowerCase().includes(q)) - .map(p => p.prefix); + .filter(p => + p.prefix.includes(q) || + p.name.toLowerCase().includes(q) || + ((p as any).description as string | undefined)?.toLowerCase().includes(q) + ) + .map(p => ({ prefix: p.prefix, description: (p as any).description ?? p.name })); return { success: false, error: String(e), + hint: 'Use searchOntologies to find the correct prefix, then retry.', ...(suggestions.length ? { suggestions } : {}), }; } @@ -457,6 +498,13 @@ const help: McpTool = { 'error with data.lateResult=true. Do NOT retry — a [Ontosphere — late result for ] follow-up', 'will be injected automatically when the operation completes.', '', + 'ONTOLOGY DISCOVERY', + 'OWL, RDFS, RDF, and XSD axioms are always pre-loaded — you can use owl:, rdfs:, rdf:, xsd: immediately.', + 'All other ontologies must be loaded explicitly before use:', + ' 1. searchOntologies("your use case") — find the right prefix (e.g. "calendar" → ical, "music" → mo, "building" → bot)', + ' 2. loadOntology("") — load into the TBox. Repeat for each domain you need.', + ' 3. Only then start addNode / addLink — types will resolve correctly.', + '', 'GRAPH ARCHITECTURE', 'Asserted triples live in urn:vg:data — all mutation tools (addNode, addLink, updateNode, SPARQL CONSTRUCT, etc.) operate here only.', 'Inferred triples live in urn:vg:inferred — written by runReasoning, cleared by clearInferred, and read-only from all other tools.', @@ -479,6 +527,7 @@ const help: McpTool = { // Exports // --------------------------------------------------------------------------- export const graphTools: McpTool[] = [ + searchOntologies, loadRdf, loadOntology, queryGraph, From 648f70bb4928188db454d98e6a7d3c8289b44fd8 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:13:36 +0200 Subject: [PATCH 18/22] refactor(ontologies): extract searchWellKnownOntologies, eliminate filter duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single exported function in wellKnownOntologies.ts. AutoComplete and searchOntologies MCP tool both call it — one code path. --- src/components/ui/OntologyUrlAutoComplete.tsx | 13 ++---------- src/mcp/tools/graph.ts | 21 +++---------------- src/utils/wellKnownOntologies.ts | 12 +++++++++++ 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/components/ui/OntologyUrlAutoComplete.tsx b/src/components/ui/OntologyUrlAutoComplete.tsx index 48312b01..c47965c0 100644 --- a/src/components/ui/OntologyUrlAutoComplete.tsx +++ b/src/components/ui/OntologyUrlAutoComplete.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useLayoutEffect, useEffect, useMemo } from 'react'; import ReactDOM from 'react-dom'; -import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from '../../utils/wellKnownOntologies'; +import { searchWellKnownOntologies, resolveOntologyLoadUrl } from '../../utils/wellKnownOntologies'; import { cn } from '../../lib/utils'; interface Props { @@ -19,16 +19,7 @@ export default function OntologyUrlAutoComplete({ value, onChange, placeholder, const listRef = useRef(null); const closeTimer = useRef | null>(null); - const filtered = useMemo(() => { - const q = query.trim().toLowerCase(); - if (!q) return WELL_KNOWN_PREFIXES; - return WELL_KNOWN_PREFIXES.filter(e => - e.prefix.toLowerCase().includes(q) || - e.name.toLowerCase().includes(q) || - e.url.toLowerCase().includes(q) || - ((e as any).description as string | undefined)?.toLowerCase().includes(q) - ); - }, [query]); + const filtered = useMemo(() => searchWellKnownOntologies(query), [query]); useEffect(() => { setActiveIndex(-1); }, [filtered]); diff --git a/src/mcp/tools/graph.ts b/src/mcp/tools/graph.ts index 0ebba1dd..3836e35b 100644 --- a/src/mcp/tools/graph.ts +++ b/src/mcp/tools/graph.ts @@ -5,7 +5,7 @@ import { rdfManager } from '@/utils/rdfManager'; import { getWorkspaceRefs, applyViewMode } from '@/mcp/workspaceContext'; import { mcpManifest, mcpServerDescription } from '@/mcp/manifest'; import { Parser as SparqlParser, Generator as SparqlGenerator } from 'sparqljs'; -import { resolveOntologyLoadUrl, WELL_KNOWN_PREFIXES } from '@/utils/wellKnownOntologies'; +import { resolveOntologyLoadUrl, searchWellKnownOntologies } from '@/utils/wellKnownOntologies'; import { useSettingsStore } from '@/stores/settingsStore'; /** Prepend PREFIX declarations from the namespace map for any prefix not already declared in the query. */ @@ -99,16 +99,7 @@ const searchOntologies: McpTool = { }, async handler(params): Promise { const { query = '' } = (params ?? {}) as { query?: string }; - const q = query.trim().toLowerCase(); - const matches = q - ? WELL_KNOWN_PREFIXES.filter(e => - e.prefix.toLowerCase().includes(q) || - e.name.toLowerCase().includes(q) || - ((e as any).description as string | undefined)?.toLowerCase().includes(q) - ) - : [...WELL_KNOWN_PREFIXES]; - - const ontologies = matches.map(e => ({ + const ontologies = searchWellKnownOntologies(query).map(e => ({ prefix: e.prefix, name: e.name, description: (e as any).description ?? '', @@ -156,13 +147,7 @@ const loadOntology: McpTool = { await rdfManager.loadRDFFromUrl(resolvedUrl, { corsProxyUrl }); return { success: true, data: { loaded: resolvedUrl, requestedAs: url !== resolvedUrl ? url : undefined } }; } catch (e) { - const q = url.toLowerCase(); - const suggestions = WELL_KNOWN_PREFIXES - .filter(p => - p.prefix.includes(q) || - p.name.toLowerCase().includes(q) || - ((p as any).description as string | undefined)?.toLowerCase().includes(q) - ) + const suggestions = searchWellKnownOntologies(url) .map(p => ({ prefix: p.prefix, description: (p as any).description ?? p.name })); return { success: false, diff --git a/src/utils/wellKnownOntologies.ts b/src/utils/wellKnownOntologies.ts index 125fd51e..e3f56de1 100644 --- a/src/utils/wellKnownOntologies.ts +++ b/src/utils/wellKnownOntologies.ts @@ -424,6 +424,18 @@ export const WELL_KNOWN_BY_PREFIX: Record< * (e.g. BFO, DCAT) that URL is returned; otherwise the namespace `url` is used. * Unrecognised strings are returned as-is so callers can pass raw URIs directly. */ +/** Filter the registry by keyword or use-case phrase. Empty query returns all entries. */ +export function searchWellKnownOntologies(query: string): typeof WELL_KNOWN_PREFIXES[number][] { + const q = query.trim().toLowerCase(); + if (!q) return [...WELL_KNOWN_PREFIXES]; + return WELL_KNOWN_PREFIXES.filter(e => + e.prefix.toLowerCase().includes(q) || + e.name.toLowerCase().includes(q) || + e.url.toLowerCase().includes(q) || + ((e as any).description as string | undefined)?.toLowerCase().includes(q) + ); +} + export function resolveOntologyLoadUrl(prefixOrUri: string): string { // Match by prefix name first const byPrefix = WELL_KNOWN_BY_PREFIX[prefixOrUri]; From 6331958fe691c8ec311eafae6775529889130ed3 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:19:41 +0200 Subject: [PATCH 19/22] chore(mcp): regenerate mcp.json (searchOntologies + updated tool descriptions) --- public/.well-known/mcp.json | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/public/.well-known/mcp.json b/public/.well-known/mcp.json index e75c24f4..cc1264c5 100644 --- a/public/.well-known/mcp.json +++ b/public/.well-known/mcp.json @@ -1,7 +1,20 @@ { "name": "Ontosphere", - "description": "Interactive RDF/ontology knowledge graph editor — ABox authoring, OWL-RL reasoning, layout, and export. All client-side, no backend.\n\nABOX vs TBOX — TWO SEPARATE CANVAS VIEWS (critical for AI agents):\nOntosphere maintains a strict ABox/TBox split. Every node you add is classified by its rdf:type and appears in exactly one view:\n • ABox view (\"abox\") — individuals and instance data. Nodes typed as owl:NamedIndividual, skos:Concept, or any non-schema type. This is the default view for authoring instance knowledge.\n • TBox view (\"tbox\") — ontology schema. Nodes typed as owl:Class, owl:ObjectProperty, owl:DatatypeProperty, owl:AnnotationProperty, rdfs:Class, etc. This view shows the schema/vocabulary layer.\n • \"Punned\" resources (typed as both, e.g. owl:Class AND owl:NamedIndividual) appear in both views.\nSwitching views replaces the entire canvas — ABox nodes are invisible in TBox view and vice versa. Use setViewMode before exportImage to capture the right layer. addNode writes triples to the store and the canvas populates automatically in the correct view.\n\nArchitecture for AI agents: The app has two coupled layers. (1) N3 RDF store (urn:vg:data) — source of truth for all triples. addNode/addLink write here first. (2) Reactodia canvas — mirrors the store subset matching the active view as draggable node cards and arrows. Nodes start collapsed; call expandNode or expandAll to reveal annotation property cards. OWL-RL reasoning writes inferred triples back to the store (urn:vg:inferred) and refreshes the canvas.\n\nRecommended workflow: setViewMode(\"tbox\") → loadOntology → addNode ×N (owl:Class etc.) → addLink ×N (subClassOf etc.) → runLayout → setViewMode(\"abox\") → addNode ×N (individuals) → addLink ×N → runLayout → runReasoning → fitCanvas → exportImage(svg).\n\nAgent integration: (1) Claude Code / Playwright — call window.__mcpTools[name](params) via browser_evaluate. (2) AI Relay Bridge — any AI chat (ChatGPT, Claude.ai, Gemini) can control Ontosphere via a bookmarklet relay that intercepts JSON-RPC 2.0 tool calls and injects results back automatically; see docs/relay-bridge.md. Full agent guide: AGENTS.md. Example sessions with SVG snapshots: docs/mcp-demo/.", + "description": "Interactive RDF/ontology knowledge graph editor — ABox authoring, OWL-RL reasoning, layout, and export. All client-side, no backend.\n\nABOX vs TBOX — TWO SEPARATE CANVAS VIEWS (critical for AI agents):\nOntosphere maintains a strict ABox/TBox split. Every node you add is classified by its rdf:type and appears in exactly one view:\n • ABox view (\"abox\") — individuals and instance data. Nodes typed as owl:NamedIndividual, skos:Concept, or any non-schema type. This is the default view for authoring instance knowledge.\n • TBox view (\"tbox\") — ontology schema. Nodes typed as owl:Class, owl:ObjectProperty, owl:DatatypeProperty, owl:AnnotationProperty, rdfs:Class, etc. This view shows the schema/vocabulary layer.\n • \"Punned\" resources (typed as both, e.g. owl:Class AND owl:NamedIndividual) appear in both views.\nSwitching views replaces the entire canvas — ABox nodes are invisible in TBox view and vice versa. Use setViewMode before exportImage to capture the right layer. addNode writes triples to the store and the canvas populates automatically in the correct view.\n\nArchitecture for AI agents: The app has two coupled layers. (1) N3 RDF store (urn:vg:data) — source of truth for all triples. addNode/addLink write here first. (2) Reactodia canvas — mirrors the store subset matching the active view as draggable node cards and arrows. Nodes start collapsed; call expandNode or expandAll to reveal annotation property cards. OWL-RL reasoning writes inferred triples back to the store (urn:vg:inferred) and refreshes the canvas.\n\nONTOLOGY DISCOVERY — always the first step:\nOWL, RDFS, RDF, and XSD are pre-loaded. All other ontologies must be loaded explicitly:\n 1. searchOntologies(\"use case\") — find the right prefix (\"calendar\" → ical, \"music\" → mo, \"building\" → bot, \"e-commerce\" → gr, …)\n 2. loadOntology(\"\") — load into TBox. Repeat for each domain.\n 3. Register a namespace prefix: addNamespace(prefix, namespace) if you want short-form IRIs.\n\nRecommended workflow: searchOntologies → loadOntology ×N → addNamespace ×N → setViewMode(\"tbox\") → addNode ×N (owl:Class etc.) → addLink ×N (subClassOf etc.) → runLayout → setViewMode(\"abox\") → addNode ×N (individuals) → addLink ×N → runLayout → runReasoning → fitCanvas → exportImage(svg).\n\nAgent integration: (1) Claude Code / Playwright — call window.__mcpTools[name](params) via browser_evaluate. (2) AI Relay Bridge — any AI chat (ChatGPT, Claude.ai, Gemini) can control Ontosphere via a bookmarklet relay that intercepts JSON-RPC 2.0 tool calls and injects results back automatically; see docs/relay-bridge.md. Full agent guide: AGENTS.md. Example sessions with SVG snapshots: docs/mcp-demo/.", "tools": [ + { + "name": "searchOntologies", + "description": "Search the well-known ontology registry by keyword or use-case phrase. Returns prefix, name, description, namespace URI, and load URL for each match. Call this FIRST to discover which ontologies cover your domain — e.g. \"calendar\" → ical, \"music\" → mo, \"building\" → bot, \"spatial\" → geo, \"e-commerce\" → gr, \"citation\" → cito. OWL/RDFS/RDF/XSD are always pre-loaded and do not appear as actionable results. Leave query empty to list all ~55 registered ontologies.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Keyword or use-case phrase. Leave empty for full listing." + } + } + } + }, { "name": "loadRdf", "description": "Load ABox instance data — subjects appear as canvas nodes. Use for individual/data triples. To load a schema or ontology without adding canvas nodes, use loadOntology instead. OWL RESTRICTIONS: loadRdf is the only way to assert axioms that require blank nodes, such as owl:someValuesFrom / owl:allValuesFrom / owl:hasValue restrictions and owl:equivalentClass with anonymous class expressions. Pattern (Turtle): `:MyClass owl:equivalentClass [ a owl:Restriction ; owl:onProperty :hasP ; owl:someValuesFrom :C ] .` The blank-node restriction individual appears in the TBox canvas view after loading.", @@ -31,7 +44,7 @@ }, { "name": "loadOntology", - "description": "Load TBox ontology for type hints and reasoning support. Does NOT add canvas nodes. Use for schema/class definitions. To load instance data as canvas nodes, use loadRdf instead.", + "description": "Load a TBox ontology by prefix name, namespace URI, or direct file URL. Does NOT add canvas nodes — use loadRdf for instance data. Call searchOntologies first to find the right prefix for your use case. Examples: loadOntology(\"ical\") for calendar events, loadOntology(\"mo\") for music, loadOntology(\"bot\") for buildings, loadOntology(\"gr\") for e-commerce.", "inputSchema": { "type": "object", "properties": { @@ -595,4 +608,4 @@ } } ] -} \ No newline at end of file +} From 360c9945792c3363b74dc510bd4e414ecaf01248 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:21:53 +0200 Subject: [PATCH 20/22] refactor(mcp): merge searchOntologies into loadOntology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadOntology now handles three modes: - url → load by prefix/URL - query → search registry by keyword - neither → list all ~55 ontologies If url doesn't resolve, load fails and suggestions from the keyword search are returned automatically — so loadOntology({url:"calendar"}) suggests ical. Removes searchOntologies as a separate tool (32 tools total). mcp.json regen. --- public/.well-known/mcp.json | 15 +------- src/mcp/manifest.ts | 23 ++++--------- src/mcp/tools/graph.ts | 69 ++++++++++++++----------------------- 3 files changed, 33 insertions(+), 74 deletions(-) diff --git a/public/.well-known/mcp.json b/public/.well-known/mcp.json index cc1264c5..206263b0 100644 --- a/public/.well-known/mcp.json +++ b/public/.well-known/mcp.json @@ -2,19 +2,6 @@ "name": "Ontosphere", "description": "Interactive RDF/ontology knowledge graph editor — ABox authoring, OWL-RL reasoning, layout, and export. All client-side, no backend.\n\nABOX vs TBOX — TWO SEPARATE CANVAS VIEWS (critical for AI agents):\nOntosphere maintains a strict ABox/TBox split. Every node you add is classified by its rdf:type and appears in exactly one view:\n • ABox view (\"abox\") — individuals and instance data. Nodes typed as owl:NamedIndividual, skos:Concept, or any non-schema type. This is the default view for authoring instance knowledge.\n • TBox view (\"tbox\") — ontology schema. Nodes typed as owl:Class, owl:ObjectProperty, owl:DatatypeProperty, owl:AnnotationProperty, rdfs:Class, etc. This view shows the schema/vocabulary layer.\n • \"Punned\" resources (typed as both, e.g. owl:Class AND owl:NamedIndividual) appear in both views.\nSwitching views replaces the entire canvas — ABox nodes are invisible in TBox view and vice versa. Use setViewMode before exportImage to capture the right layer. addNode writes triples to the store and the canvas populates automatically in the correct view.\n\nArchitecture for AI agents: The app has two coupled layers. (1) N3 RDF store (urn:vg:data) — source of truth for all triples. addNode/addLink write here first. (2) Reactodia canvas — mirrors the store subset matching the active view as draggable node cards and arrows. Nodes start collapsed; call expandNode or expandAll to reveal annotation property cards. OWL-RL reasoning writes inferred triples back to the store (urn:vg:inferred) and refreshes the canvas.\n\nONTOLOGY DISCOVERY — always the first step:\nOWL, RDFS, RDF, and XSD are pre-loaded. All other ontologies must be loaded explicitly:\n 1. searchOntologies(\"use case\") — find the right prefix (\"calendar\" → ical, \"music\" → mo, \"building\" → bot, \"e-commerce\" → gr, …)\n 2. loadOntology(\"\") — load into TBox. Repeat for each domain.\n 3. Register a namespace prefix: addNamespace(prefix, namespace) if you want short-form IRIs.\n\nRecommended workflow: searchOntologies → loadOntology ×N → addNamespace ×N → setViewMode(\"tbox\") → addNode ×N (owl:Class etc.) → addLink ×N (subClassOf etc.) → runLayout → setViewMode(\"abox\") → addNode ×N (individuals) → addLink ×N → runLayout → runReasoning → fitCanvas → exportImage(svg).\n\nAgent integration: (1) Claude Code / Playwright — call window.__mcpTools[name](params) via browser_evaluate. (2) AI Relay Bridge — any AI chat (ChatGPT, Claude.ai, Gemini) can control Ontosphere via a bookmarklet relay that intercepts JSON-RPC 2.0 tool calls and injects results back automatically; see docs/relay-bridge.md. Full agent guide: AGENTS.md. Example sessions with SVG snapshots: docs/mcp-demo/.", "tools": [ - { - "name": "searchOntologies", - "description": "Search the well-known ontology registry by keyword or use-case phrase. Returns prefix, name, description, namespace URI, and load URL for each match. Call this FIRST to discover which ontologies cover your domain — e.g. \"calendar\" → ical, \"music\" → mo, \"building\" → bot, \"spatial\" → geo, \"e-commerce\" → gr, \"citation\" → cito. OWL/RDFS/RDF/XSD are always pre-loaded and do not appear as actionable results. Leave query empty to list all ~55 registered ontologies.", - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Keyword or use-case phrase. Leave empty for full listing." - } - } - } - }, { "name": "loadRdf", "description": "Load ABox instance data — subjects appear as canvas nodes. Use for individual/data triples. To load a schema or ontology without adding canvas nodes, use loadOntology instead. OWL RESTRICTIONS: loadRdf is the only way to assert axioms that require blank nodes, such as owl:someValuesFrom / owl:allValuesFrom / owl:hasValue restrictions and owl:equivalentClass with anonymous class expressions. Pattern (Turtle): `:MyClass owl:equivalentClass [ a owl:Restriction ; owl:onProperty :hasP ; owl:someValuesFrom :C ] .` The blank-node restriction individual appears in the TBox canvas view after loading.", @@ -44,7 +31,7 @@ }, { "name": "loadOntology", - "description": "Load a TBox ontology by prefix name, namespace URI, or direct file URL. Does NOT add canvas nodes — use loadRdf for instance data. Call searchOntologies first to find the right prefix for your use case. Examples: loadOntology(\"ical\") for calendar events, loadOntology(\"mo\") for music, loadOntology(\"bot\") for buildings, loadOntology(\"gr\") for e-commerce.", + "description": "Discover or load well-known ontologies — three modes in one tool. (1) Load: pass url with a prefix name (e.g. \"ical\", \"mo\", \"bot\", \"gr\") or a namespace/file URL — loads the TBox, does NOT add canvas nodes. (2) Search: pass query with a use-case keyword (\"calendar\", \"music\", \"building\", \"e-commerce\", \"spatial\", \"IoT\") — returns matching entries with prefix/description/loadUrl. (3) List all: pass neither — returns all ~55 registered ontologies. If url does not match a known prefix or URL, load fails and suggestions are returned automatically. OWL/RDFS/RDF/XSD are always pre-loaded.", "inputSchema": { "type": "object", "properties": { diff --git a/src/mcp/manifest.ts b/src/mcp/manifest.ts index e187d132..7b5e0695 100644 --- a/src/mcp/manifest.ts +++ b/src/mcp/manifest.ts @@ -21,20 +21,6 @@ export const mcpServerDescription = 'Agent integration: (1) Claude Code / Playwright — call window.__mcpTools[name](params) via browser_evaluate. (2) AI Relay Bridge — any AI chat (ChatGPT, Claude.ai, Gemini) can control Ontosphere via a bookmarklet relay that intercepts JSON-RPC 2.0 tool calls and injects results back automatically; see docs/relay-bridge.md. Full agent guide: AGENTS.md. Example sessions with SVG snapshots: docs/mcp-demo/.'; export const mcpManifest: McpToolManifestEntry[] = [ - { - name: 'searchOntologies', - description: - 'Search the well-known ontology registry by keyword or use-case phrase. Returns prefix, name, description, namespace URI, and load URL for each match. ' + - 'Call this FIRST to discover which ontologies cover your domain — e.g. "calendar" → ical, "music" → mo, "building" → bot, "spatial" → geo, "e-commerce" → gr, "citation" → cito. ' + - 'OWL/RDFS/RDF/XSD are always pre-loaded and do not appear as actionable results. ' + - 'Leave query empty to list all ~55 registered ontologies.', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Keyword or use-case phrase. Leave empty for full listing.' }, - }, - }, - }, { name: 'loadRdf', description: @@ -57,9 +43,12 @@ export const mcpManifest: McpToolManifestEntry[] = [ { name: 'loadOntology', description: - 'Load a TBox ontology by prefix name, namespace URI, or direct file URL. Does NOT add canvas nodes — use loadRdf for instance data. ' + - 'Call searchOntologies first to find the right prefix for your use case. ' + - 'Examples: loadOntology("ical") for calendar events, loadOntology("mo") for music, loadOntology("bot") for buildings, loadOntology("gr") for e-commerce.', + 'Discover or load well-known ontologies — three modes in one tool. ' + + '(1) Load: pass url with a prefix name (e.g. "ical", "mo", "bot", "gr") or a namespace/file URL — loads the TBox, does NOT add canvas nodes. ' + + '(2) Search: pass query with a use-case keyword ("calendar", "music", "building", "e-commerce", "spatial", "IoT") — returns matching entries with prefix/description/loadUrl. ' + + '(3) List all: pass neither — returns all ~55 registered ontologies. ' + + 'If url does not match a known prefix or URL, load fails and suggestions are returned automatically. ' + + 'OWL/RDFS/RDF/XSD are always pre-loaded.', inputSchema: { type: 'object', properties: { diff --git a/src/mcp/tools/graph.ts b/src/mcp/tools/graph.ts index 3836e35b..58e6bb6e 100644 --- a/src/mcp/tools/graph.ts +++ b/src/mcp/tools/graph.ts @@ -82,65 +82,49 @@ const loadRdf: McpTool = { }, }; -// --------------------------------------------------------------------------- -// searchOntologies -// --------------------------------------------------------------------------- -const searchOntologies: McpTool = { - name: 'searchOntologies', - description: 'Search the well-known ontology registry by keyword or use case. Returns matching ontologies with prefix, name, description, namespace URI, and load URL. Call this to discover which ontologies to load before calling loadOntology.', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'Keyword or use-case phrase (e.g. "calendar", "music", "building", "e-commerce", "citation", "spatial", "IoT"). Leave empty to list all registered ontologies.', - }, - }, - }, - async handler(params): Promise { - const { query = '' } = (params ?? {}) as { query?: string }; - const ontologies = searchWellKnownOntologies(query).map(e => ({ - prefix: e.prefix, - name: e.name, - description: (e as any).description ?? '', - namespace: e.url, - loadUrl: resolveOntologyLoadUrl(e.prefix), - })); - - return { - success: true, - data: { query: query || '(all)', count: ontologies.length, ontologies }, - }; - }, -}; - // --------------------------------------------------------------------------- // loadOntology // --------------------------------------------------------------------------- const loadOntology: McpTool = { name: 'loadOntology', description: - 'Load a well-known ontology by prefix name (e.g. "bibo", "ro", "foaf", "saref"), ' + - 'by its namespace URL, or by any direct ontology file URL. ' + - 'Use searchOntologies first to discover the right prefix for your use case.', + 'Discover or load well-known ontologies. ' + + 'Pass url to load by prefix name (e.g. "ical", "mo", "bot", "gr") or by namespace/file URL. ' + + 'Pass query to search by use-case keyword (e.g. "calendar", "music", "building", "e-commerce"). ' + + 'Pass neither to list all ~55 registered ontologies. ' + + 'OWL/RDFS/RDF/XSD are always pre-loaded.', inputSchema: { type: 'object', properties: { url: { type: 'string', - description: - 'Prefix name (e.g. "bibo"), namespace IRI, or direct ontology URL.', + description: 'Prefix name, namespace IRI, or direct ontology URL to load.', + }, + query: { + type: 'string', + description: 'Keyword or use-case phrase to search the registry (e.g. "calendar", "IoT", "spatial").', }, }, - required: ['url'], }, async handler(params): Promise { - const { url = '' } = (params ?? {}) as { url?: string }; + const { url, query } = (params ?? {}) as { url?: string; query?: string }; - if (!url.trim()) { - return { success: false, error: 'url is required. Use searchOntologies to discover available ontologies.' }; + // Search mode + if (!url?.trim()) { + const ontologies = searchWellKnownOntologies(query ?? '').map(e => ({ + prefix: e.prefix, + name: e.name, + description: (e as any).description ?? '', + namespace: e.url, + loadUrl: resolveOntologyLoadUrl(e.prefix), + })); + return { + success: true, + data: { query: query || '(all)', count: ontologies.length, ontologies }, + }; } + // Load mode const resolvedUrl = resolveOntologyLoadUrl(url); const corsProxyUrl = useSettingsStore.getState().settings.corsProxyUrl; try { @@ -152,7 +136,7 @@ const loadOntology: McpTool = { return { success: false, error: String(e), - hint: 'Use searchOntologies to find the correct prefix, then retry.', + hint: 'Pass query instead of url to search the registry.', ...(suggestions.length ? { suggestions } : {}), }; } @@ -512,7 +496,6 @@ const help: McpTool = { // Exports // --------------------------------------------------------------------------- export const graphTools: McpTool[] = [ - searchOntologies, loadRdf, loadOntology, queryGraph, From f91ce39967ef8deb6c6ea9a0ccc80ce0e0a2c5be Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:24:56 +0200 Subject: [PATCH 21/22] feat(startup): per-ontology sonner toasts for ?ontology= URL parameter loads Each comma-separated entry now resolves via WELL_KNOWN_BY_PREFIX (so "foaf" shows "FOAF" not the raw URL), loads individually, and fires a distinct toast.success / toast.error with description "from ?ontology= parameter". --- src/components/Canvas/ReactodiaCanvas.tsx | 40 ++++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index ea7bdfa3..8a7feb6a 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -42,7 +42,7 @@ import { Label } from '../ui/label'; import { Input } from '../ui/input'; import OntologyUrlAutoComplete from '../ui/OntologyUrlAutoComplete'; import { Button } from '../ui/button'; -import { WELL_KNOWN_PREFIXES, resolveOntologyLoadUrl } from '@/utils/wellKnownOntologies'; +import { WELL_KNOWN_BY_PREFIX, resolveOntologyLoadUrl } from '@/utils/wellKnownOntologies'; import { instantiateWorkflowOnCanvas } from '@/utils/workflowInstantiator'; function extractNamespace(iri: string): string { @@ -855,15 +855,20 @@ export default function ReactodiaCanvas() { } // ?ontology= comma-separated list of well-known prefix names (e.g. "bfo,dcat") or full URIs. - let startupOntologyUrls: string[] = []; + let startupOntologyEntries: { input: string; resolved: string; label: string }[] = []; try { const u = new URL(String(window.location.href)); const ontologyParam = u.searchParams.get('ontology') || u.searchParams.get('ontologies') || ''; if (ontologyParam.trim()) { - startupOntologyUrls = ontologyParam + startupOntologyEntries = ontologyParam .split(',') - .map((s) => resolveOntologyLoadUrl(s.trim())) - .filter(Boolean); + .map((s) => s.trim()) + .filter(Boolean) + .map((s) => ({ + input: s, + resolved: resolveOntologyLoadUrl(s), + label: WELL_KNOWN_BY_PREFIX[s]?.name ?? s, + })); } } catch { /* ignore */ @@ -904,18 +909,21 @@ export default function ReactodiaCanvas() { } } - // Load ontologies specified via ?ontology= URL param (additive — runs alongside all other mechanisms). - if (startupOntologyUrls.length > 0) { - try { - actions.setLoading(true, 5, 'Loading ontologies from URL parameter...'); - await loadAdditionalOntologies(startupOntologyUrls, (progress: number, message: string) => { - actions.setLoading(true, Math.max(5, progress), message); - }); - } catch (err) { - console.warn('[ReactodiaCanvas] ?ontology= load failed', err); - } finally { - actions.setLoading(false, 0, ''); + // Load ontologies specified via ?ontology= URL param — one toast per entry. + if (startupOntologyEntries.length > 0) { + for (const entry of startupOntologyEntries) { + try { + actions.setLoading(true, 5, `Loading ${entry.label}...`); + await loadAdditionalOntologies([entry.resolved], (progress, message) => { + actions.setLoading(true, Math.max(5, progress), message); + }); + toast.success(entry.label, { description: 'Loaded from ?ontology= parameter' }); + } catch (err) { + console.warn('[ReactodiaCanvas] ?ontology= load failed', entry.input, err); + toast.error(`Failed to load ${entry.label}`, { description: '?ontology= parameter — check the prefix or URL' }); + } } + actions.setLoading(false, 0, ''); } })(); }, [loadKnowledgeGraph, loadAdditionalOntologies, actions]); From dfd4d04bc23c0b901ea81dba439e81c0a04275b8 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Wed, 29 Apr 2026 10:27:08 +0200 Subject: [PATCH 22/22] fix(ui): improve sonner toasts for ontology/data load paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?url= startup: show filename + "?url= parameter" description; add error toast on failure (was console.error only). Manual load dialog: resolve well-known name from namespace URL (via WELL_KNOWN_BY_PREFIX), fall back to URL filename. Drop redundant error toast — ontologyStore already fires a detailed CORS-aware one. --- src/components/Canvas/ReactodiaCanvas.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/Canvas/ReactodiaCanvas.tsx b/src/components/Canvas/ReactodiaCanvas.tsx index 8a7feb6a..35822475 100644 --- a/src/components/Canvas/ReactodiaCanvas.tsx +++ b/src/components/Canvas/ReactodiaCanvas.tsx @@ -901,8 +901,13 @@ export default function ReactodiaCanvas() { disableImportDiscovery: !loadImportsEnabledRef.current, ...(startupApiKey ? { apiKey: startupApiKey, apiKeyHeader: startupApiKeyHeader || undefined } : {}), }); - toast.success('Startup knowledge graph loaded'); + const startupLabel = (() => { + try { return new URL(startupUrl).pathname.split('/').filter(Boolean).pop()?.replace(/\.[^.]+$/, '') || startupUrl; } + catch { return startupUrl; } + })(); + toast.success(`Loaded: ${startupLabel}`, { description: '?url= parameter' }); } catch (err) { + toast.error('Failed to load startup graph', { description: startupUrl }); console.error('[ReactodiaCanvas] Startup URL load failed', err); } finally { actions.setLoading(false, 0, ''); @@ -1465,12 +1470,17 @@ export default function ReactodiaCanvas() { await loadAdditionalOntologies([url], (progress, message) => { actions.setLoading(true, Math.max(progress, 30), message); }); - toast.success('Ontology loaded successfully'); + const wk = Object.values(WELL_KNOWN_BY_PREFIX).find(e => e.url === url); + const loadedLabel = wk?.name ?? (() => { + try { return new URL(url).pathname.split('/').filter(Boolean).pop()?.replace(/\.[^.]+$/, '') || url; } + catch { return url; } + })(); + toast.success(`Loaded: ${loadedLabel}`); setOntologyUrlInput(''); setLoadOntologyOpen(false); } catch (err) { + // ontologyStore already fires a detailed CORS-aware error toast; this is a fallback console.error('Failed to load ontology:', err); - toast.error('Failed to load ontology'); } finally { actions.setLoading(false, 0, ''); }