diff --git a/public/app/axiolotl-inference.js b/public/app/axiolotl-inference.js new file mode 100644 index 0000000..4da0c59 --- /dev/null +++ b/public/app/axiolotl-inference.js @@ -0,0 +1,753 @@ +// axiolotl-inference.js + +// Dependencies + // comunica-indexeddb-bridge.js + // applyUpdateWithComunica + // semantic-core.js + // downloadText(filename, text, mime) + +/** + * Extract selected inference rule IDs from checked checkboxes. + * @returns {string[]} List of rule identifiers + */ +function getSelectedRulesFromCheckboxes() { + return Array.from(document.querySelectorAll('input[name="inference-rule"]:checked')) + .map(el => el.value); +} + +// Build adjacency maps from the RDF/JS store (across all graphs) +function mapFromQuads(store, predIRI) { + const { namedNode } = N3.DataFactory; + const M = new Map(); + + for (const q of store.getQuads(null, namedNode(predIRI), null, null)) { + const a = q.subject.value; + const b = q.object.value; + if (!M.has(a)) M.set(a, new Set()); + M.get(a).add(b); + } + return M; +} + +// Generic transitive-closure over a directed acyclic-ish relation (rdfs:subClassOf, rdfs:subPropertyOf) +function transitiveClosure(edges) { + const closure = new Map(); + for (const [child, parents] of edges) { + const seenLocal = new Set(); + const stack = [...parents]; + while (stack.length) { + const p = stack.pop(); + if (seenLocal.has(p)) continue; + seenLocal.add(p); + const pp = edges.get(p); + if (pp) pp.forEach(x => stack.push(x)); + } + closure.set(child, seenLocal); + } + return closure; +} + +// TBox IRIs +const RDFS_SC = 'http://www.w3.org/2000/01/rdf-schema#subClassOf'; +const RDFS_SP = 'http://www.w3.org/2000/01/rdf-schema#subPropertyOf'; +const OWL_INV = 'http://www.w3.org/2002/07/owl#inverseOf'; +const OWL_SYM = 'http://www.w3.org/2002/07/owl#SymmetricProperty'; +const OWL_TRANS = 'http://www.w3.org/2002/07/owl#TransitiveProperty'; +const RDFS_DOMAIN = 'http://www.w3.org/2000/01/rdf-schema#domain'; +const RDFS_RANGE = 'http://www.w3.org/2000/01/rdf-schema#range'; +const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; + +/** + * Clears the Inference Engine console + */ +function clearInferenceConsole() { + const box = document.getElementById('inference-console'); + if (box) box.value = ''; +} + +function appendInferenceConsoleLine(message) { + const box = document.getElementById('inference-console'); + if (!box) return; + box.value += `${message}\n`; + box.scrollTop = box.scrollHeight; +} + +function setInferenceBusy(isBusy) { + const spinner = document.getElementById('inference-spinner'); + if (!spinner) return; + + spinner.classList.toggle('is-busy', !!isBusy); +} + +window.appendInferenceConsoleLine = appendInferenceConsoleLine; +window.clearInferenceConsole = clearInferenceConsole; +window.setInferenceBusy = setInferenceBusy; + +function inferenceInfo(message) { + if (debuggingConsoleEnabled) console.info(message); + if (typeof window !== 'undefined' && typeof window.appendInferenceConsoleLine === 'function') { + window.appendInferenceConsoleLine(message); + } +} + +function inferenceWarn(message) { + if (debuggingConsoleEnabled) console.warn(message); + if (typeof window !== 'undefined' && typeof window.appendInferenceConsoleLine === 'function') { + window.appendInferenceConsoleLine(`WARN: ${message}`); + } +} + +function inferenceError(message) { + if (debuggingConsoleEnabled) console.error(message); + if (typeof window !== 'undefined' && typeof window.appendInferenceConsoleLine === 'function') { + window.appendInferenceConsoleLine(`ERROR: ${message}`); + } +} + +// set up the key:value structure +function quadKey(q) { + return [ + q.subject.termType, q.subject.value, + q.predicate.termType, q.predicate.value, + q.object.termType, q.object.value, + q.object.language || '', + q.object.datatype?.value || '', + q.graph.termType, q.graph.value || '' + ].join('¦'); +} + +function canBeSubject(term) { + return !!term && (term.termType === 'NamedNode' || term.termType === 'BlankNode'); +} +/** + * Applies a set of inference rules repeatedly until no new triples are added. + * @param {string[]} rules - List of rule identifiers to apply (e.g. ["inverse", "subclassof"]) + * @returns {quad} quas + * Event-driven: new ABox assertions immediately trigger subclass/subproperty, + * inverse/symmetric, and domain/range expansions using precomputed TBox closures. +*/ +// axiolotl-inference.js +async function inferUntilStable(rules) { + if (debuggingConsoleEnabled) { + console.info('[inferUntilStable] Starting inference over rules:', rules); + } + + const { DataFactory, Store } = N3; + const { namedNode, quad } = DataFactory; + + // ---- 0) Load dataset and set up dedupe ---- + const baseStore = await loadGraphFromIndexedDB(); + const rdfjsStore = baseStore; // mutate in place as closure/rules add quads + const overlayStore = new Store(); // only newly inferred quads + const seen = new Set(rdfjsStore.getQuads(null, null, null, null).map(quadKey)); + + // ---- 1) Precompute TBox closures/maps ---- + const subClassEdges = mapFromQuads(rdfjsStore, RDFS_SC); + const subPropEdges = mapFromQuads(rdfjsStore, RDFS_SP); + + const classSupers = transitiveClosure(subClassEdges); // Map Set> + const propSupers = transitiveClosure(subPropEdges); // Map Set> + + const domainMap = mapFromQuads(rdfjsStore, RDFS_DOMAIN); // Map Set> + const rangeMap = mapFromQuads(rdfjsStore, RDFS_RANGE); // Map Set> + + const symmetricProps = new Set( + rdfjsStore + .getQuads(null, namedNode(RDF_TYPE), namedNode(OWL_SYM), null) + .map(q => q.subject.value) + ); + + const transitiveProps = new Set( + rdfjsStore + .getQuads(null, namedNode(RDF_TYPE), namedNode(OWL_TRANS), null) + .map(q => q.subject.value) + ); + + // Make owl:inverseOf two-way + const inversePairs = new Map(); // Map

Set> + for (const q of rdfjsStore.getQuads(null, namedNode(OWL_INV), null, null)) { + const p = q.subject.value; + const inv = q.object.value; + + if (!inversePairs.has(p)) inversePairs.set(p, new Set()); + if (!inversePairs.has(inv)) inversePairs.set(inv, new Set()); + + inversePairs.get(p).add(inv); + inversePairs.get(inv).add(p); + } + + // ---- 2) Work queues and enqueue logic ---- + const workTypes = rdfjsStore.getQuads(null, namedNode(RDF_TYPE), null, null).slice(); + const workProps = rdfjsStore + .getQuads(null, null, null, null) + .filter(q => q.predicate.value !== RDF_TYPE); + + function enqueue(quads) { + for (const q of quads) { + const key = quadKey(q); + if (seen.has(key)) continue; + + rdfjsStore.addQuad(q); + overlayStore.addQuad(q); + seen.add(key); + + if (q.predicate.value === RDF_TYPE) workTypes.push(q); + else workProps.push(q); + } + } + + function expandTypesWithClosure(newTypes) { + const out = []; + let skipped = 0; + + for (const q of newTypes) { + const c = q.object.value; + const supers = classSupers.get(c); + if (!supers) continue; + + for (const sup of supers) { + if (typeof isAbsoluteIri === 'function' && !isAbsoluteIri(sup)) { + skipped++; + continue; + } + + out.push(quad( + q.subject, + namedNode(RDF_TYPE), + namedNode(sup), + q.graph + )); + } + } + + if (debuggingConsoleEnabled && skipped) { + console.warn(`[expandTypesWithClosure] Skipped ${skipped} non-IRI super-classes`); + } + + return out; + } + + function expandPropsWithClosure(newProps) { + const out = []; + let skipped = 0; + + for (const q of newProps) { + const p = q.predicate.value; + const supers = propSupers.get(p); + if (!supers) continue; + + for (const sup of supers) { + if (typeof isAbsoluteIri === 'function' && !isAbsoluteIri(sup)) { + skipped++; + continue; + } + + out.push(quad( + q.subject, + namedNode(sup), + q.object, + q.graph + )); + } + } + + if (debuggingConsoleEnabled && skipped) { + console.warn(`[expandPropsWithClosure] Skipped ${skipped} non-IRI super-properties`); + } + + return out; + } + + function applyInverseAndSymmetric(newProps) { + const out = []; + + for (const q of newProps) { + const p = q.predicate.value; + + if (!canBeSubject(q.object)) continue; + + if (symmetricProps.has(p)) { + out.push(quad( + q.object, + namedNode(p), + q.subject, + q.graph + )); + } + + const invs = inversePairs.get(p); + if (invs) { + for (const inv of invs) { + out.push(quad( + q.object, + namedNode(inv), + q.subject, + q.graph + )); + } + } + } + + return out; + } + + function applyDomainRange(newProps) { + const out = []; + let skipDom = 0; + let skipRng = 0; + + for (const q of newProps) { + const p = q.predicate.value; + + const Ds = domainMap.get(p); + if (Ds) { + for (const d of Ds) { + if (typeof isAbsoluteIri === 'function' && !isAbsoluteIri(d)) { + skipDom++; + continue; + } + + out.push(quad( + q.subject, + namedNode(RDF_TYPE), + namedNode(d), + q.graph + )); + } + } + + const Rs = rangeMap.get(p); + if (Rs && canBeSubject(q.object)) { + for (const r of Rs) { + if (typeof isAbsoluteIri === 'function' && !isAbsoluteIri(r)) { + skipRng++; + continue; + } + + out.push(quad( + q.object, + namedNode(RDF_TYPE), + namedNode(r), + q.graph + )); + } + } + } + + if (debuggingConsoleEnabled && skipDom) { + console.warn(`[applyDomainRange] Skipped ${skipDom} domain classes that were not IRIs`); + } + if (debuggingConsoleEnabled && skipRng) { + console.warn(`[applyDomainRange] Skipped ${skipRng} range classes that were not IRIs`); + } + + return out; + } + + function applyTransitiveProps(newPropsBatch) { + const out = []; + + for (const q of newPropsBatch) { + const p = q.predicate.value; + if (!transitiveProps.has(p)) continue; + + const pred = namedNode(p); + + // x p y & y p z -> x p z + if (canBeSubject(q.object)) { + for (const yz of rdfjsStore.getQuads(q.object, pred, null, q.graph)) { + out.push(quad( + q.subject, + pred, + yz.object, + q.graph + )); + } + } + + // w p x & x p y -> w p y + for (const wx of rdfjsStore.getQuads(null, pred, q.subject, q.graph)) { + out.push(quad( + wx.subject, + pred, + q.object, + q.graph + )); + } + } + + return out; + } + + function processQueues() { + let progressed = false; + + while (workTypes.length || workProps.length) { + if (workTypes.length) { + const batch = workTypes.splice(0, workTypes.length); + const extra = expandTypesWithClosure(batch); + if (extra.length) { + enqueue(extra); + progressed = true; + } + } + + if (workProps.length) { + const batch = workProps.splice(0, workProps.length); + const extra1 = expandPropsWithClosure(batch); + const extra2 = applyInverseAndSymmetric(batch); + const extra3 = applyDomainRange(batch); + const extra4 = applyTransitiveProps(batch); + + if (extra1.length) { enqueue(extra1); progressed = true; } + if (extra2.length) { enqueue(extra2); progressed = true; } + if (extra3.length) { enqueue(extra3); progressed = true; } + if (extra4.length) { enqueue(extra4); progressed = true; } + } + } + + return progressed; + } + + // ---- 3) Seed closures from existing dataset ---- + let totalAdded = 0; + let pass = 0; + const MAX_PASSES = 100; + + const seedSeenBefore = seen.size; + processQueues(); + const seedAdded = seen.size - seedSeenBefore; + totalAdded += seedAdded; + + inferenceInfo(`[inferUntilStable] Seed closures added ${seedAdded} triples.`); + + let changed = true; + + while (changed) { + pass += 1; + if (pass > MAX_PASSES) { + throw new Error(`[inferUntilStable] Aborted after ${MAX_PASSES} passes. Likely non-stable loop.`); + } + + changed = false; + let passAdded = 0; + + inferenceInfo(`[inferUntilStable] Starting pass ${pass}...`); + + const rulesFiltered = rules.filter(r => r !== 'subclassof' && r !== 'subpropertyof'); + if (rules.length !== rulesFiltered.length) { + inferenceInfo('[inferUntilStable] Skipping SPARQL for subclassof/subpropertyof (handled by JS closures)'); + } + + for (const rule of rulesFiltered) { + const constructQuery = getConstructQueryForRule(rule); + if (!constructQuery) continue; + + const newQuads = await runRuleOnce(rule, rdfjsStore); + + const batch = []; + const batchSeen = new Set(); + for (const q of newQuads) { + const key = quadKey(q); + if (batchSeen.has(key)) continue; + batchSeen.add(key); + batch.push(q); + } + + const beforeDirect = seen.size; + enqueue(batch); + const directAdded = seen.size - beforeDirect; + + const beforeClosure = seen.size; + processQueues(); + const closureAdded = seen.size - beforeClosure; + + const ruleAdded = directAdded + closureAdded; + passAdded += ruleAdded; + + inferenceInfo( + `[inferUntilStable] Pass ${pass}, rule "${rule}": direct=${directAdded}, propagated=${closureAdded}, total=${ruleAdded}` + ); + } + + if (passAdded > 0) { + totalAdded += passAdded; + changed = true; + inferenceInfo(`[inferUntilStable] Completed pass ${pass}: added ${passAdded} triples.`); + } else { + inferenceInfo(`[inferUntilStable] Completed pass ${pass}: no new triples. Stable.`); + } + } + + const overlayCount = overlayStore.getQuads(null, null, null, null).length; + inferenceInfo( + `[inferUntilStable] Completed inference. Passes=${pass}, total new triples=${totalAdded}, overlay triples=${overlayCount}` + ); + + return { + overlayGraph: overlayStore, + metrics: { totalAdded, passes: pass, overlayCount } + }; +} + +/** + * Run inference (until stable) to produce an overlay dataset; optionally persist overlay. + * @param {Object} opt + * @param {string[]} opt.rules + * @param {'default'|'named'} [opt.targetMode='default'] + * @param {string|null} [opt.graphIRI=null] + * @param {boolean} [opt.persist=false] + * @returns {Promise<{overlayGraph:N3.Store, metrics:Object, count?:number, graphIRI?:string}>} + */ +async function runInferenceOverlay(opt={}) { + const { rules=[], targetMode='default', graphIRI=null, persist=false } = opt; + if (typeof inferUntilStable !== 'function') { + throw new Error('runInferenceOverlay requires inferUntilStable'); + } + const { overlayGraph, metrics } = await inferUntilStable(rules); + if (!persist) return { overlayGraph, metrics }; + + const res = await stashGraphToIndexedDB(overlayGraph, targetMode, graphIRI, 'urn:graph:inferred'); + return { overlayGraph, metrics, count: res.count, graphIRI: res.graphIRI }; +} + + +/** + * De-duplicate a batch and remove quads already present in the global `seen` set. + * @param {Array} quads + * @param {Set} seenKeys + * @returns {{ $new: Array, batchUnique: number }} + */ +function selectUnseen(quads, seenKeys) { + const batchSet = new Set(); + const uniq = []; + + for (const q of quads) { + const key = quadKey(q); + if (batchSet.has(key)) continue; + batchSet.add(key); + if (!seenKeys.has(key)) uniq.push(q); + } + + return { $new: uniq, batchUnique: batchSet.size }; +} + +function pad2(n){ return String(n).padStart(2,'0'); } + +/** + * Return a stable UTC timestamp: YYYYMMDDThhmmssZ + */ +function timestampUTC() { + const d = new Date(); + const pad = n => String(n).padStart(2,'0'); + return `${d.getUTCFullYear()}${pad(d.getUTCMonth()+1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`; +} + +/** + * Return a UUID (uses crypto.randomUUID if available). + * Pure; no side effects. + */ +function uuid() { + if (crypto && typeof crypto.randomUUID === 'function') return crypto.randomUUID(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random()*16|0, v = (c === 'x') ? r : ((r & 0x3) | 0x8); + return v.toString(16); + }); +} + +async function insertOverlayIntoEndpoint(overlayGraph, endpointUrl, { mode, graphIRI }) { + if (!overlayGraph) throw new Error('Nothing to insert. Run inference first.'); + if (!endpointUrl) throw new Error('Missing endpoint URL.'); + + const { Writer, DataFactory } = N3; + const { quad, defaultGraph } = DataFactory; + + // Flatten to triples for INSERT DATA; target graph is controlled by mode/graphIRI. + const flattened = overlayGraph + .getQuads(null, null, null, null) + .map(q => quad(q.subject, q.predicate, q.object, defaultGraph())); + + const nt = await new Promise((resolve, reject) => { + const writer = new Writer({ format: 'N-Triples' }); + writer.addQuads(flattened); + writer.end((error, result) => { + if (error) reject(error); + else resolve((result || '').trim()); + }); + }); + + const open = (mode === 'named' && graphIRI) ? `GRAPH <${graphIRI}> {` : ''; + const close = (mode === 'named' && graphIRI) ? `}` : ''; + const update = `INSERT DATA { ${open}\n${nt}\n${close} }`; + + const res = await fetch(endpointUrl, { + method: 'POST', + headers: { 'content-type': 'application/sparql-update', ...endpointAuthHeaders }, + body: update + }); + + if (!res.ok) { + const t = await res.text().catch(() => ''); + throw new Error(`Endpoint responded ${res.status}: ${t || res.statusText}`); + } + + return true; +} + +/** + * Returns a SPARQL CONSTRUCT string for a given rule name. + * Each rule is a single CONSTRUCT query (no semicolons between queries). + * Uses MINUS to return only triples not already present. + * @param {string} rule - Rule identifier + * @returns {string} SPARQL CONSTRUCT query + */ +function getConstructQueryForRule(rule) { + const PREFIXES = ` + PREFIX rdf: + PREFIX rdfs: + PREFIX owl: + `; + + const RULES = { + // Inverse properties: produce either ?y ?inverse ?x or ?y ?p ?x depending on branch. + // We normalize to (?S ?P ?O) via BIND and construct once. + inverse: ` + CONSTRUCT { ?S ?P ?O } + WHERE { + { + ?x ?p ?y . + ?p owl:inverseOf ?inverse . + BIND(?y AS ?S) BIND(?inverse AS ?P) BIND(?x AS ?O) + } + UNION + { + ?x ?inverse ?y . + ?p owl:inverseOf ?inverse . + BIND(?y AS ?S) BIND(?p AS ?P) BIND(?x AS ?O) + } + FILTER NOT EXISTS { + { ?S ?P ?O } + UNION + { GRAPH ?g { ?S ?P ?O } } + } + } + `, + + subpropertyof: ` + CONSTRUCT { ?x ?super ?y } + WHERE { + ?x ?p ?y . + ?p rdfs:subPropertyOf+ ?super . + FILTER(?p != ?super) + FILTER NOT EXISTS { + { ?x ?super ?y } + UNION + { GRAPH ?g { ?x ?super ?y } } + } + } + `, + + subclassof: ` + CONSTRUCT { ?x rdf:type ?superClass } + WHERE { + ?x rdf:type ?class . + ?class rdfs:subClassOf+ ?superClass . + FILTER(?class != ?superClass) + FILTER NOT EXISTS { + { ?x rdf:type ?superClass } + UNION + { GRAPH ?g { ?x rdf:type ?superClass } } + } + } + `, + + domain: ` + CONSTRUCT { ?x rdf:type ?domain } + WHERE { + ?x ?p ?y . + ?p rdfs:domain ?domain . + FILTER NOT EXISTS { + { ?x rdf:type ?domain } + UNION + { GRAPH ?g { ?x rdf:type ?domain } } + } + } + `, + + range: ` + CONSTRUCT { ?y rdf:type ?range } + WHERE { + ?x ?p ?y . + ?p rdfs:range ?range . + FILTER NOT EXISTS { + { ?y rdf:type ?range } + UNION + { GRAPH ?g { ?y rdf:type ?range } } + } + } + `, + + transitive: ` + CONSTRUCT { ?x ?p ?z } + WHERE { + ?x ?p ?y . + ?y ?p ?z . + ?p a owl:TransitiveProperty . + FILTER NOT EXISTS { + { ?x ?p ?z } + UNION + { GRAPH ?g { ?x ?p ?z } } + } + } + `, + symmetric: ` + CONSTRUCT { ?y ?p ?x } + WHERE { + ?x ?p ?y . + ?p a owl:SymmetricProperty . + FILTER NOT EXISTS { + { ?y ?p ?x } + UNION + { GRAPH ?g { ?y ?p ?x } } + } + } + `, + }; + + return PREFIXES + (RULES[rule] || ''); +} + +/** + * Runs one inference rule once and returns newly inferred quads. + * @param {string} rule + * @param {N3.Store} rdfjsStore + * @returns {Promise>} + */ +async function runRuleOnce(rule, rdfjsStore) { + const q = getConstructQueryForRule(rule); + if (!q) return []; + return await applyConstructWithComunica(q, rdfjsStore); +} + +/** + * Applies a SPARQL CONSTRUCT query into the rdfjsStore + * @param {*} constructQuery + * @param {*} rdfjsStore + * @returns + */ +async function applyConstructWithComunica(constructQuery, rdfjsStore) { + const quadStream = await engine.queryQuads(constructQuery, { + sources: [{ type: 'rdfjsSource', value: rdfjsStore }], + baseIRI: 'http://example.org/', + distinctConstruct: true, + }); + + return await new Promise((resolve, reject) => { + const quads = []; + quadStream.on('data', q => quads.push(q)); + quadStream.on('end', () => resolve(quads)); + quadStream.on('error', reject); + }); +} + +window.applyConstructWithComunica = applyConstructWithComunica; diff --git a/public/lib/axiolotl-query.js b/public/app/axiolotl-query.js similarity index 95% rename from public/lib/axiolotl-query.js rename to public/app/axiolotl-query.js index a7202c8..55c0b7e 100644 --- a/public/lib/axiolotl-query.js +++ b/public/app/axiolotl-query.js @@ -56,23 +56,59 @@ const defaultActivePrefixes = ['rdfs', 'owl', 'skos']; * window.__lastOverlayGraph exists * element with id="rdf-preview" exists * getSelectedOutputMime() function exists - * $rdf.serialize function exists * @returns */ -function updatePreviewFromOverlay() { +async function updatePreviewFromOverlay() { const g = window.__lastOverlayGraph; const box = document.getElementById('rdf-preview'); if (!g || !box) return; + try { - const mime = getSelectedOutputMime(); // turtle, n-triples, etc. - const text = $rdf.serialize(null, g, 'http://example.org/', mime); + const mime = getSelectedOutputMime(); + const text = await serializeStore(g, mime); box.value = text; } catch (e) { - if (debuggingConsoleEnabled) {console.error('[updatePreviewFromOverlay] serialize error:', e);} + if (debuggingConsoleEnabled) { + console.error('[updatePreviewFromOverlay] serialize error:', e); + } box.value = `Serialization error: ${e && (e.message || e)}`; } } +async function serializeStore(store, mime = 'text/turtle') { + if (!store || typeof store.getQuads !== 'function') { + throw new Error('serializeStore expected an N3.Store or compatible RDF/JS source.'); + } + + const supported = new Set([ + 'text/turtle', + 'application/n-triples', + 'application/n-quads' + ]); + + const format = supported.has(mime) ? mime : 'text/turtle'; + + return await new Promise((resolve, reject) => { + const writer = new N3.Writer({ format }); + writer.addQuads(store.getQuads(null, null, null, null)); + writer.end((error, result) => { + if (error) reject(error); + else resolve(result || ''); + }); + }); +} + +async function serializeStoreToNTriples(store) { + return await new Promise((resolve, reject) => { + const writer = new N3.Writer({ format: 'N-Triples' }); + writer.addQuads(store.getQuads(null, null, null, null)); + writer.end((error, result) => { + if (error) reject(error); + else resolve(result || ''); + }); + }); +} + /** * Get/set active prefixes from localStorage * Assumes: @@ -203,17 +239,20 @@ async function handleRunInference() { const { overlayGraph, metrics } = await inferUntilStable(selectedRules, baseIRI, overlayIRI); - // Serialize and display preview - const previewText = await serializeTurtle(overlayGraph); + window.__lastOverlayGraph = overlayGraph; + + const previewText = await serializeStore(overlayGraph, getSelectedOutputMime()); document.getElementById('rdf-preview').value = previewText; - // Save overlay graph await stashGraphToIndexedDB(overlayGraph, 'named', overlayIRI); - // Log metrics - if (debuggingConsoleEnabled) {console.info('[handleRunInference] Inference metrics:', metrics);} + if (debuggingConsoleEnabled) { + console.info('[handleRunInference] Inference metrics:', metrics); + } } catch (error) { - if (debuggingConsoleEnabled) {console.error('[handleRunInference] Failed:', error);} + if (debuggingConsoleEnabled) { + console.error('[handleRunInference] Failed:', error); + } } } @@ -248,23 +287,33 @@ function getSaveTarget() { // Save inferred overlay graph to IndexedDB async function runInference() { -try { + try { + clearInferenceConsole?.(); + setInferenceBusy(true); + const rules = getSelectedRulesFromCheckboxes(); const { overlayGraph, metrics } = await inferUntilStable(rules); + window.__lastOverlayGraph = overlayGraph; - updatePreviewFromOverlay(); + await updatePreviewFromOverlay(); + + const n = overlayGraph.getQuads(null, null, null, null).length; - const n = overlayGraph.statements.length; showToast( - n ? `Inference finished — ${n} triple${n === 1 ? '' : 's'} materialized.` + n + ? `Inference finished — ${n} triple${n === 1 ? '' : 's'} materialized.` : 'Inference finished — no new triples.', n ? 'success' : 'info' ); } catch (err) { - if (debuggingConsoleEnabled) {console.error('[run-inference] failed:', err);} + if (debuggingConsoleEnabled) { + console.error('[run-inference] failed:', err); + } showToast(`Inference error: ${err.message || err}`, 'error'); + } finally { + setInferenceBusy(false); } -}; +} // Insert overlay graph into SPARQL endpoint async function saveOverlayToIndexedDB(overlayGraph, { mode, graphIRI }) { @@ -306,25 +355,29 @@ async function insertInferredTriplesIntoEndpoint() { }; // Export inferred overlay graph as a file in chosen format -function exportInferredOverlay() { +async function exportInferredOverlay() { try { const g = window.__lastOverlayGraph; if (!g) throw new Error('Nothing to export. Run inference first.'); + const mime = getSelectedOutputMime(); - const text = serializeGraph(g, mime); + const text = await serializeStore(g, mime); + const ext = ({ 'text/turtle': 'ttl', 'application/n-triples': 'nt', - 'application/ld+json': 'jsonld', - 'application/rdf+xml': 'rdf' + 'application/n-quads': 'nq' })[mime] || 'ttl'; + downloadText(`inferred-${timestampUTC()}.${ext}`, text, mime); showToast('Download started.', 'success'); } catch (e) { - if (debuggingConsoleEnabled) {console.error(e);} + if (debuggingConsoleEnabled) { + console.error(e); + } showToast(e.message || String(e), 'error'); } -}; +} // Dynamically add file + IRI input rows function createFileInputRow(index) { @@ -378,8 +431,12 @@ toggleReasonerButtons(); document.getElementById('run-inference')?.addEventListener('click', runInference); document.getElementById('save-inferred-to-db')?.addEventListener('click', saveInferredTriplesToDB); document.getElementById('insert-inferred-to-endpoint')?.addEventListener('click', insertInferredTriplesIntoEndpoint); -document.getElementById('export-inferred')?.addEventListener('click', exportInferredOverlay); -document.getElementById('output-format')?.addEventListener('change', updatePreviewFromOverlay); +document.getElementById('export-inferred')?.addEventListener('click', async () => { + await exportInferredOverlay(); +}); +document.getElementById('output-format')?.addEventListener('change', async () => { + await updatePreviewFromOverlay(); +}); // Event handler for adding new rows document.getElementById('add-file-row').addEventListener('click', addNewFileRow); @@ -542,6 +599,7 @@ function initTabs() { // UI event bindings window.addEventListener('DOMContentLoaded', () => { + setInferenceBusy(false); initTabs(); document.getElementById('file-upload')?.addEventListener('change', async (e) => { for (const file of e.target.files) { @@ -751,12 +809,13 @@ const commitUpdateByMaterialization = async (updateStr, targetMode='default') => if (insQs.length) { for (const q of insQs) { const ttl = await runConstructPreview(q, 'text/turtle'); - const overlay = $rdf.graph(); - await new Promise((resolve, reject) => { - $rdf.parse(ttl, overlay, 'http://example.org/', 'text/turtle', err => err ? reject(err) : resolve()); - }); + + const parser = new N3.Parser({ format: 'text/turtle', baseIRI: 'http://example.org/' }); + const quads = parser.parse(ttl); + const overlay = new N3.Store(quads); + await stashGraphToIndexedDB(overlay, targetMode, graphIRI); - inserted += overlay.statements.length; + inserted += quads.length; } } @@ -820,7 +879,7 @@ async function renderOntologyList() { const warnMissing = !fileName || !dataPath; const li = document.createElement('li'); - li.style.marginLeft = '0.4em'; + li.style.marginLeft = '1.5em'; li.style.marginBottom = '0.4em'; li.innerHTML = `

IRI Path Finder

- - - - - - + + + + + + + + + + + + + + + + +
- + Clears all triples from the default graph and any named graphs.
- + Clears all user-defined SPARQL queries.
- + Clears SPARQL endpoint settings, prefix bindings, and other preferences.
- + Deletes Active Workspace database (IndexedDB) and localStorage. Hard reset. @@ -1326,11 +1427,11 @@

Add Top- and Mid-level Ontologies

@@ -1418,46 +1519,52 @@

Saved Queries

Select Inference Rules:

    -
  • -
  • -
  • -
  • +
  • -
  • -
  • -
- Select DB:
+ Select DB to Reason Over:

+ id="reasoner-source-endpoint" value="endpoint"> SPARQL Endpoint

Inference Engine: Forward-Chain Reasoning

- +
+
+ Inference log +   + +
+ +
+
+ Output preview: