UNPKG

unrdf

Version:

Production-ready RDF knowledge graph library with Knowledge Hooks, SPARC methodology, and Knowledge Substrate optimization

1,522 lines (1,512 loc) â€ĸ 74.3 kB
#!/usr/bin/env node import { RdfEngine } from "./_chunks/rdf-engine-H5lGYQ__.mjs"; import { defineCommand, runMain } from "citty"; import { createContext } from "unctx"; import { AsyncLocalStorage } from "node:async_hooks"; import { DataFactory, Store as Store$1 } from "n3"; import rdfCanonize from "rdf-canonize"; import crypto, { createHash } from "node:crypto"; import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { z } from "zod"; import { randomUUID } from "crypto"; import "path"; import "fs"; import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; //#region src/context/index.mjs const { namedNode: namedNode$2, literal, blankNode: blankNode$1, quad, defaultGraph } = DataFactory; /** * Store context interface - SENDER operations with optional READER operations * * UNRDF enforces a sender-only model for core operations. Reader operations * are provided as optional methods but return placeholder/empty results in * strict sender-only mode to maintain the principle of unidirectional data flow. * * @typedef {Object} StoreContext * @property {Function} add - Add quads to store (SENDER - PRIMARY) * @property {Function} remove - Remove quads from store (SENDER - PRIMARY) * @property {Function} clear - Clear all quads (SENDER - PRIMARY) * @property {Function} namedNode - Create named node (SENDER - PRIMARY) * @property {Function} literal - Create literal (SENDER - PRIMARY) * @property {Function} blankNode - Create blank node (SENDER - PRIMARY) * @property {Function} quad - Create quad (SENDER - PRIMARY) * @property {Function} [serialize] - Serialize store (READER - OPTIONAL - returns placeholder) * @property {Function} [stats] - Get store statistics (READER - OPTIONAL - returns zeros) * @property {Function} [query] - Execute SPARQL queries (READER - OPTIONAL - returns empty results) * @property {Function} [canonicalize] - Canonicalize store (READER - OPTIONAL - returns placeholder) * @property {Function} [isIsomorphic] - Check store isomorphism (READER - OPTIONAL - returns false) * @property {Function} [hash] - Generate canonical hash (READER - OPTIONAL - returns placeholder) */ /** * Create the root store context using unctx with native async context support * This enables context preservation across async operations */ const storeContext = createContext({ asyncContext: true, AsyncLocalStorage }); /** * Hook to access the store context * @returns {StoreContext} Current store context * * @throws {Error} If store context is not initialized */ const useStoreContext = storeContext.use; /** * Create a store context instance * @param {Array<Quad>} [initialQuads=[]] - Initial quads * @param {Object} [options] - Store options * @returns {StoreContext} Store context */ function createStoreContext(initialQuads = [], options = {}) { if (!Array.isArray(initialQuads)) throw new TypeError("[createStoreContext] initialQuads must be an array"); if (options && typeof options !== "object") throw new TypeError("[createStoreContext] options must be an object"); const engine = new RdfEngine(options); const store = engine.getStore(); if (initialQuads.length > 0) store.addQuads(initialQuads); const context = { engine, store, add(...quads) { for (const q of quads) { if (q === null || q === void 0) throw new TypeError("[StoreContext] Cannot add null or undefined quad"); if (typeof q !== "object" || !q.termType) throw new TypeError("[StoreContext] Invalid quad: must have termType property"); store.add(q); } return this; }, remove(...quads) { for (const q of quads) { if (q === null || q === void 0) throw new TypeError("[StoreContext] Cannot remove null or undefined quad"); if (typeof q !== "object" || !q.termType) throw new TypeError("[StoreContext] Invalid quad: must have termType property"); store.delete(q); } return this; }, clear() { store.removeQuads([...store]); return this; }, namedNode(value) { if (typeof value !== "string") throw new TypeError("[StoreContext] namedNode value must be a string"); return namedNode$2(value); }, literal(value, datatype) { if (typeof value !== "string") throw new TypeError("[StoreContext] literal value must be a string"); return literal(value, datatype); }, blankNode(value) { if (value !== void 0 && typeof value !== "string") throw new TypeError("[StoreContext] blankNode value must be a string"); return blankNode$1(value); }, quad(s, p, o, g) { if (!s || !p || !o) throw new TypeError("[StoreContext] quad requires subject, predicate, and object"); return quad(s, p, o, g || defaultGraph()); }, async serialize(options$1 = {}) { if (options$1 && typeof options$1 !== "object") throw new TypeError("[StoreContext] serialize options must be an object"); const { format = "Turtle", prefixes } = options$1; if (format === "Turtle") return this.engine.serializeTurtle(store, { prefixes }); if (format === "N-Quads") return this.engine.serializeNQuads(store); throw new Error(`[StoreContext] Unsupported serialization format: ${format}`); }, stats() { return { quads: 0, subjects: 0, predicates: 0, objects: 0, graphs: 0 }; }, query(sparql, options$1 = {}) { if (typeof sparql !== "string" || !sparql.trim()) throw new Error("query: non-empty SPARQL required"); const q = sparql.trim(); if (!q) throw new Error("query: non-empty SPARQL required"); console.log("DEBUG: storeContext query called with engine:", typeof this.engine, this.engine?.constructor?.name); console.log("DEBUG: engine has queryBoolean:", typeof this.engine?.queryBoolean); const kind = q.toUpperCase().match(/\b(SELECT|ASK|CONSTRUCT|DESCRIBE|WITH|INSERT|DELETE|LOAD|CREATE|DROP|CLEAR|MOVE|COPY|ADD)\b/)?.[1]; if (!kind) throw new Error("query: unknown query type - only SELECT, ASK, CONSTRUCT, DESCRIBE supported"); if (/^(WITH|INSERT|DELETE|LOAD|CREATE|DROP|CLEAR|MOVE|COPY|ADD)$/i.test(kind)) try { const result = this.engine.query(sparql, options$1); return result; } catch (error) { throw new Error(`UPDATE operation failed: ${error.message}`); } const limit = Number.isFinite(options$1.limit) ? options$1.limit : Infinity; const deterministic = options$1.deterministic ?? false; try { const result = this.engine.query(sparql, options$1); if (result.type === "select") return { type: "select", variables: this._getColumns(sparql), rows: result.results || [] }; return result; } catch (error) { throw new Error(`Query failed: ${error.message}`); } }, _sortQuads(quads) { return quads.sort((a, b) => { const aStr = `${a.subject.value} ${a.predicate.value} ${a.object.value} ${a.graph.value || ""}`; const bStr = `${b.subject.value} ${b.predicate.value} ${b.object.value} ${b.graph.value || ""}`; return aStr.localeCompare(bStr); }); }, _getColumns(query) { const selectMatch = query.match(/SELECT\s+(?:\w+\s+)*(\?\w+(?:\s+\(\w+\([^)]*\)\s+as\s+\?\w+\))?)/i); if (selectMatch) { const selectClause = selectMatch[1]; const columns = selectClause.match(/\?\w+/g); return columns || []; } return []; }, _termToJSON(term) { if (!term) return null; const termData = { type: term.termType, value: term.value }; if (term.termType === "Literal") { if (term.language) termData.language = term.language; if (term.datatype) termData.datatype = term.datatype.value; } return termData; }, _variable(name) { return engine.variable ? engine.variable(name) : { value: name }; }, async canonicalize(options$1 = {}) { const { timeoutMs = 3e4, onMetric } = options$1; try { const canonize = rdfCanonize.canonize; const nquads = this.serialize({ format: "N-Quads" }); const canonical = canonize(nquads, { algorithm: "URDNA2015", format: "application/n-quads", produceGeneralizedRdf: false }); if (onMetric) onMetric("canonicalization", { duration: Date.now() - Date.now(), size: nquads.length }); return canonical; } catch (error) { throw new Error(`Canonicalization failed: ${error.message}`); } }, async isIsomorphic(store1, store2, options$1 = {}) { const { timeoutMs = 3e4 } = options$1; try { const canonize = rdfCanonize.canonize; const canonical1 = canonize(this.serialize({ format: "N-Quads" }), { algorithm: "URDNA2015", format: "application/n-quads" }); const canonical2 = canonize(store2 ? this.serializeStore(store2, { format: "N-Quads" }) : "", { algorithm: "URDNA2015", format: "application/n-quads" }); return canonical1 === canonical2; } catch (error) { throw new Error(`Isomorphism check failed: ${error.message}`); } }, async hash(options$1 = {}) { const { algorithm = "SHA-256" } = options$1; try { const canonical = this.canonicalize(); const hash = crypto.createHash(algorithm.toLowerCase()); hash.update(canonical); return hash.digest("hex"); } catch (error) { throw new Error(`Hash generation failed: ${error.message}`); } }, async serializeStore(store$1, options$1 = {}) { const { format = "Turtle", prefixes } = options$1; if (format === "Turtle") return this.engine.serializeTurtle(store$1, { prefixes }); if (format === "N-Quads") return this.engine.serializeNQuads(store$1); throw new Error(`Unsupported serialization format: ${format}`); } }; return context; } /** * Initialize the root store context * This should be called at the root of your application * * @param {Array<Quad>} [initialQuads=[]] - Initial quads * @param {Object} [options] - Store options * @returns {Function} Function to call with your application logic * * @example * // At the root of your application * const runApp = initStore([], { baseIRI: 'http://example.org/' }); * * runApp(() => { * // Your application code here * const store = useStoreContext(); * // All composables will use the same store * }); */ function initStore(initialQuads = [], options = {}) { const context = createStoreContext(initialQuads, options); return (fn) => { return storeContext.callAsync(context, fn); }; } //#endregion //#region src/composables/use-graph.mjs /** * Create a graph composable for operating on the global RDF store * * @returns {Object} Graph operations interface * * @example * // Initialize store context first * const runApp = initStore(); * * runApp(() => { * const graph = useGraph(); * * // SPARQL SELECT query * const results = graph.select(` * PREFIX ex: <http://example.org/> * SELECT ?s ?p ?o WHERE { ?s ?p ?o } * `); * * // SPARQL ASK query * const exists = graph.ask(` * PREFIX ex: <http://example.org/> * ASK { ex:subject ex:predicate ?o } * `); * }); * * @throws {Error} If store context is not initialized */ function useGraph() { const storeContext$1 = useStoreContext(); const engine = storeContext$1.engine; const store = storeContext$1.store; return { query(sparql, options) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); return storeContext$1.query(sparql, options); }, select(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = storeContext$1.query(sparql); if (res.type !== "select") throw new Error("[useGraph] Query is not a SELECT query"); return res.results || []; }, ask(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); console.log("DEBUG: useGraph.ask called with:", sparql.substring(0, 50) + "..."); console.log("DEBUG: storeContext type:", typeof storeContext$1); console.log("DEBUG: storeContext has query:", typeof storeContext$1.query); const res = storeContext$1.query(sparql); if (res.type !== "ask") throw new Error("[useGraph] Query is not an ASK query"); return res.boolean || false; }, construct(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = storeContext$1.query(sparql); if (res.type !== "construct") throw new Error("[useGraph] Query is not a CONSTRUCT query"); return res.store || new Store(); }, update(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = storeContext$1.query(sparql); if (res.type !== "update") throw new Error("[useGraph] Query is not an UPDATE query"); return res; }, validate(shapesInput) { return engine.validateShacl(store, shapesInput); }, validateOrThrow(shapesInput) { const result = engine.validateShacl(store, shapesInput); if (!result.conforms) throw new Error(`SHACL validation failed: ${result.results.map((r) => r.message).join(", ")}`); return result; }, serialize(options = {}) { if (options && typeof options !== "object") throw new TypeError("[useGraph] serialize options must be an object"); const { format = "Turtle", prefixes } = options; if (format === "Turtle") return engine.serializeTurtle(store, { prefixes }); if (format === "N-Quads") return engine.serializeNQuads(store); throw new Error(`[useGraph] Unsupported serialization format: ${format}`); }, pointer() { return engine.getClownface(store); }, stats() { return engine.getStats(store); }, isIsomorphic(otherGraph) { const otherStore = otherGraph.store || otherGraph; return engine.isIsomorphic(store, otherStore); }, union(...otherGraphs) { const otherStores = otherGraphs.map((g) => g.store || g); const resultStore = engine.union(store, ...otherStores); return createTemporaryGraph(resultStore, engine); }, difference(otherGraph) { const otherStore = otherGraph.store || otherGraph; const resultStore = engine.difference(store, otherStore); return createTemporaryGraph(resultStore, engine); }, intersection(otherGraph) { const otherStore = otherGraph.store || otherGraph; const resultStore = engine.intersection(store, otherStore); return createTemporaryGraph(resultStore, engine); }, skolemize(baseIRI) { const resultStore = engine.skolemize(store, baseIRI); return createTemporaryGraph(resultStore, engine); }, toJSONLD(options = {}) { return engine.toJSONLD(store, options); }, get size() { return store.size; } }; } /** * Create a temporary graph interface for a specific store * Used for operations that return new stores (union, difference, etc.) * @param {Store} store - The store to wrap * @param {RdfEngine} engine - The RDF engine to use * @returns {Object} Graph interface * @private */ function createTemporaryGraph(store, engine) { return { get store() { return store; }, get engine() { return engine; }, query(sparql, options) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); try { return engine.query(store, sparql, options); } catch (error) { throw new Error(`[useGraph] Query failed: ${error.message}`); } }, select(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = engine.query(store, sparql); if (res.type !== "select") throw new Error("[useGraph] Query is not a SELECT query"); return res.results; }, ask(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = engine.query(store, sparql); if (res.type !== "ask") throw new Error("[useGraph] Query is not an ASK query"); return res.boolean; }, construct(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = engine.query(store, sparql); if (res.type !== "construct") throw new Error("[useGraph] Query is not a CONSTRUCT query"); return res.store; }, update(sparql) { if (typeof sparql !== "string") throw new TypeError("[useGraph] SPARQL query must be a string"); const res = engine.query(store, sparql); if (res.type !== "update") throw new Error("[useGraph] Query is not an UPDATE query"); return res; }, validate(shapesInput) { return engine.validateShacl(store, shapesInput); }, validateOrThrow(shapesInput) { return engine.validateShaclOrThrow(store, shapesInput); }, serialize(options = {}) { if (options && typeof options !== "object") throw new TypeError("[useGraph] serialize options must be an object"); const { format = "Turtle", prefixes } = options; if (format === "Turtle") return engine.serializeTurtle(store, { prefixes }); if (format === "N-Quads") return engine.serializeNQuads(store); throw new Error(`[useGraph] Unsupported serialization format: ${format}`); }, pointer() { return engine.getClownface(store); }, stats() { return engine.getStats(store); }, async isIsomorphic(otherGraph) { const otherStore = otherGraph.store || otherGraph; return engine.isIsomorphic(store, otherStore); }, union(...otherGraphs) { const otherStores = otherGraphs.map((g) => g.store || g); const resultStore = engine.union(store, ...otherStores); return createTemporaryGraph(resultStore, engine); }, difference(otherGraph) { const otherStore = otherGraph.store || otherGraph; const resultStore = engine.difference(store, otherStore); return createTemporaryGraph(resultStore, engine); }, intersection(otherGraph) { const otherStore = otherGraph.store || otherGraph; const resultStore = engine.intersection(store, otherStore); return createTemporaryGraph(resultStore, engine); }, skolemize(baseIRI) { const resultStore = engine.skolemize(store, baseIRI); return createTemporaryGraph(resultStore, engine); }, toJSONLD(options = {}) { return engine.toJSONLD(store, options); }, get size() { return store.size; } }; } //#endregion //#region src/composables/use-turtle.mjs /** * Create a Turtle file system composable * * @param {string} [graphDir='./graph'] - Directory containing Turtle files * @param {Object} [options] - Turtle options * @param {string} [options.baseIRI] - Base IRI for parsing * @param {boolean} [options.autoLoad=true] - Automatically load all .ttl files * @param {boolean} [options.validateOnLoad=true] - Validate files on load * @returns {Object} Turtle file system interface * * @example * const turtle = useTurtle('./my-graph'); * * // Load all .ttl files * turtle.loadAll(); * * // Save a specific graph * turtle.save('my-graph', store); * * // Load a specific file * const store = turtle.load('my-graph'); */ function useTurtle(graphDir = "./graph", options = {}) { const { baseIRI = "http://example.org/", autoLoad = true, validateOnLoad = true } = options; const storeContext$1 = useStoreContext(); const engine = storeContext$1.engine; try { mkdirSync(graphDir, { recursive: true }); } catch (error) { if (error.code !== "EEXIST") throw error; } return { get store() { return storeContext$1; }, get graphDir() { return graphDir; }, get engine() { return engine; }, loadAll(options$1 = {}) { const { merge = true, validate = validateOnLoad } = options$1; try { const files = readdirSync(graphDir); const ttlFiles = files.filter((f) => f.endsWith(".ttl")); if (ttlFiles.length === 0) { console.log(`No .ttl files found in ${graphDir}`); return { loaded: 0, files: [] }; } const loadedFiles = []; for (const fileName of ttlFiles) try { const filePath = join(graphDir, fileName); const content = readFileSync(filePath, "utf8"); if (validate) engine.parseTurtle(content, { baseIRI }); if (!merge) storeContext$1.store.clear(); const parsedStore = engine.parseTurtle(content, { baseIRI }); for (const quad$1 of parsedStore) storeContext$1.store.add(quad$1); loadedFiles.push(fileName); console.log(`✅ Loaded: ${fileName}`); } catch (error) { console.warn(`âš ī¸ Failed to load ${fileName}: ${error.message}`); } console.log(`📁 Loaded ${loadedFiles.length} files from ${graphDir}`); return { loaded: loadedFiles.length, files: loadedFiles }; } catch (error) { if (error.code === "ENOENT") { console.log(`📁 Graph directory ${graphDir} doesn't exist yet`); return { loaded: 0, files: [] }; } throw error; } }, load(fileName, options$1 = {}) { const { merge = true, validate = validateOnLoad } = options$1; const filePath = join(graphDir, `${fileName}.ttl`); try { const content = readFileSync(filePath, "utf8"); if (validate) engine.parseTurtle(content, { baseIRI }); const parsedStore = engine.parseTurtle(content, { baseIRI }); if (!merge) storeContext$1.clear(); for (const quad$1 of parsedStore) storeContext$1.store.add(quad$1); console.log(`✅ Loaded: ${fileName}.ttl`); return parsedStore; } catch (error) { if (error.code === "ENOENT") throw new Error(`File not found: ${fileName}.ttl`); throw error; } }, save(fileName, options$1 = {}) { const { prefixes, createBackup = false } = options$1; const filePath = join(graphDir, `${fileName}.ttl`); try { if (createBackup) try { statSync(filePath); const backupPath = `${filePath}.backup`; const content = readFileSync(filePath, "utf8"); writeFileSync(backupPath, content, "utf8"); console.log(`📋 Created backup: ${fileName}.ttl.backup`); } catch {} const turtleContent = engine.serializeTurtle(storeContext$1.store, { prefixes }); writeFileSync(filePath, turtleContent, "utf8"); const stats = statSync(filePath); console.log(`💾 Saved: ${fileName}.ttl (${stats.size} bytes)`); return { path: filePath, bytes: stats.size }; } catch (error) { console.error(`❌ Failed to save ${fileName}.ttl:`, error.message); throw error; } }, saveDefault(options$1 = {}) { return this.save("default", { ...options$1, createBackup: true }); }, loadDefault(options$1 = {}) { try { return this.load("default", options$1); } catch (error) { if (error.message.includes("File not found")) { console.log(`â„šī¸ No default.ttl file found in ${graphDir}`); return null; } throw error; } }, listFiles() { try { const files = readdirSync(graphDir); const ttlFiles = files.filter((f) => f.endsWith(".ttl")); console.log(`📁 Found ${ttlFiles.length} .ttl files in ${graphDir}`); return ttlFiles; } catch (error) { if (error.code === "ENOENT") return []; throw error; } }, stats() { return storeContext$1.stats(); }, clear() { storeContext$1.clear(); }, parse(ttl, options$1 = {}) { const { addToStore = false,...parseOptions } = options$1; const parsedStore = engine.parseTurtle(ttl, { baseIRI, ...parseOptions }); if (addToStore) for (const quad$1 of parsedStore) storeContext$1.store.add(quad$1); return parsedStore; }, serialize(options$1 = {}) { return engine.serializeTurtle(storeContext$1.store, options$1); } }; } //#endregion //#region src/composables/use-delta.mjs /** * Create a delta composable for graph operations * * @param {Object} [options] - Delta options * @param {boolean} [options.deterministic=true] - Enable deterministic operations * @returns {Object} Delta interface * * @example * // Initialize store context first * const runApp = initStore(); * * runApp(() => { * const delta = useDelta(); * * // Compare two graphs * const result = delta.compare(sourceData, targetData); * console.log(`Added: ${result.added}, Removed: ${result.removed}`); * * // Apply changes to current store * delta.apply(result); * }); */ function useDelta(options = {}) { const { deterministic = true } = options; const storeContext$1 = useStoreContext(); const engine = storeContext$1.engine; return { compareWith(newStore) { const contextStore = storeContext$1.store; const added = new Store$1(); const removed = new Store$1(); for (const quad$1 of newStore) if (!contextStore.has(quad$1)) added.add(quad$1); for (const quad$1 of contextStore) if (!newStore.has(quad$1)) removed.add(quad$1); return { added: deterministic ? this._sortQuads(added) : added, removed: deterministic ? this._sortQuads(removed) : removed, addedCount: added.size, removedCount: removed.size, unchangedCount: contextStore.size - removed.size, contextSize: contextStore.size, newDataSize: newStore.size }; }, syncWith(newStore, options$1 = {}) { const { dryRun = false } = options$1; const changes = this.compareWith(newStore); const result = this.apply(changes, { dryRun }); return { ...result, changes }; }, apply(changes, options$1 = {}) { const { dryRun = false } = options$1; const { added, removed } = changes; const originalSize = storeContext$1.store.size; let addedCount = 0; let removedCount = 0; if (!dryRun) { for (const quad$1 of removed) if (storeContext$1.store.has(quad$1)) { storeContext$1.store.delete(quad$1); removedCount++; } for (const quad$1 of added) if (!storeContext$1.store.has(quad$1)) { storeContext$1.store.add(quad$1); addedCount++; } } else { for (const quad$1 of removed) if (storeContext$1.store.has(quad$1)) removedCount++; for (const quad$1 of added) if (!storeContext$1.store.has(quad$1)) addedCount++; } return { success: true, added: addedCount, removed: removedCount, originalSize, finalSize: storeContext$1.store.size, dryRun }; }, getStats(changes) { const { added, removed } = changes; const addedSubjects = new Set([...added].map((q) => q.subject.value)); const addedPredicates = new Set([...added].map((q) => q.predicate.value)); const addedObjects = new Set([...added].map((q) => q.object.value)); const removedSubjects = new Set([...removed].map((q) => q.subject.value)); const removedPredicates = new Set([...removed].map((q) => q.predicate.value)); const removedObjects = new Set([...removed].map((q) => q.object.value)); return { added: { quads: added.size, subjects: addedSubjects.size, predicates: addedPredicates.size, objects: addedObjects.size }, removed: { quads: removed.size, subjects: removedSubjects.size, predicates: removedPredicates.size, objects: removedObjects.size }, total: { quads: added.size + removed.size, netChange: added.size - removed.size }, coverage: { addedSubjects: Array.from(addedSubjects), removedSubjects: Array.from(removedSubjects), addedPredicates: Array.from(addedPredicates), removedPredicates: Array.from(removedPredicates) } }; }, isEmpty(changes) { const { added, removed } = changes; return added.size === 0 && removed.size === 0; }, merge(...changeSets) { const merged = { added: new Store$1(), removed: new Store$1() }; for (const changes of changeSets) { if (!changes || typeof changes !== "object") continue; const { added, removed } = changes; if (added) for (const quad$1 of added) merged.added.add(quad$1); if (removed) for (const quad$1 of removed) merged.removed.add(quad$1); } return { added: deterministic ? this._sortQuads(merged.added) : merged.added, removed: deterministic ? this._sortQuads(merged.removed) : merged.removed }; }, invert(changes) { const { added, removed } = changes; return { added: removed || new Store$1(), removed: added || new Store$1() }; }, createPatch(changes, options$1 = {}) { const { format = "Turtle" } = options$1; const { added, removed } = changes; let addedData = ""; let removedData = ""; if (format === "Turtle") { addedData = engine.serializeTurtle(added); removedData = engine.serializeTurtle(removed); } else if (format === "N-Quads") { addedData = engine.serializeNQuads(added); removedData = engine.serializeNQuads(removed); } else throw new Error(`Unsupported format: ${format}`); return { added: addedData, removed: removedData, addedCount: added.size, removedCount: removed.size, format, stats: this.getStats(changes) }; }, applyPatch(patch, options$1 = {}) { const { added, removed, format = "Turtle" } = patch; const addedStore = format === "Turtle" ? engine.parseTurtle(added) : engine.parseNQuads(added); const removedStore = format === "Turtle" ? engine.parseTurtle(removed) : engine.parseNQuads(removed); return this.apply({ added: addedStore, removed: removedStore }, options$1); }, _sortQuads(store) { const quads = [...store]; quads.sort((a, b) => { const aStr = `${a.subject.value}${a.predicate.value}${a.object.value}${a.graph.value}`; const bStr = `${b.subject.value}${b.predicate.value}${b.object.value}${b.graph.value}`; return aStr.localeCompare(bStr); }); return new Store$1(quads); } }; } //#endregion //#region src/knowledge-engine/policy-pack.mjs /** * Schema for policy pack metadata */ const PolicyPackMetaSchema = z.object({ name: z.string().min(1).max(100).regex(/^[a-zA-Z0-9:_-]+$/, "Name must contain only alphanumeric characters, colons, hyphens, and underscores"), version: z.string().regex(/^\d+\.\d+\.\d+$/, "Version must be semantic version format"), description: z.string().min(1).max(1e3).optional(), author: z.string().min(1).max(100).optional(), license: z.string().min(1).max(100).optional(), tags: z.array(z.string().min(1).max(50)).max(20).optional(), ontology: z.array(z.string().min(1).max(100)).max(10).optional(), dependencies: z.array(z.object({ name: z.string().min(1), version: z.string().min(1), required: z.boolean().default(true) })).max(20).optional(), createdAt: z.coerce.date().optional(), updatedAt: z.coerce.date().optional() }); /** * Schema for policy pack configuration */ const PolicyPackConfigSchema = z.object({ enabled: z.boolean().default(true), priority: z.number().int().min(0).max(100).default(50), strictMode: z.boolean().default(false), timeout: z.number().int().positive().max(3e5).default(3e4), retries: z.number().int().nonnegative().max(5).default(1), conditions: z.object({ environment: z.array(z.string()).optional(), version: z.string().optional(), features: z.array(z.string()).optional() }).optional() }); /** * Schema for policy pack manifest */ const PolicyPackManifestSchema = z.object({ id: z.string().uuid(), meta: PolicyPackMetaSchema, config: PolicyPackConfigSchema, hooks: z.array(z.object({ name: z.string().min(1), file: z.string().min(1), enabled: z.boolean().default(true), priority: z.number().int().min(0).max(100).default(50) })), conditions: z.array(z.object({ name: z.string().min(1), file: z.string().min(1), type: z.enum([ "sparql-ask", "sparql-select", "shacl" ]) })).optional(), resources: z.array(z.object({ name: z.string().min(1), file: z.string().min(1), type: z.enum([ "ontology", "vocabulary", "data", "other" ]) })).optional() }); /** * Create a new policy pack manifest * @param {Object} options - Manifest options * @returns {Object} Policy pack manifest */ function createPolicyPackManifest(name, hooks, options = {}) { const manifest = { id: randomUUID(), meta: { name, version: options.version || "1.0.0", description: options.description, author: options.author, license: options.license || "MIT", tags: options.tags || [], ontology: options.ontology || [], dependencies: options.dependencies || [], createdAt: new Date().toISOString() }, config: { enabled: options.enabled !== false, priority: options.priority || 50, strictMode: options.strictMode || false, timeout: options.timeout || 3e4, retries: options.retries || 1, conditions: options.conditions || {} }, hooks: hooks.map((hook) => ({ name: hook.meta.name, file: `${hook.meta.name}.mjs`, enabled: true, priority: hook.priority || 50 })), conditions: options.conditions || [], resources: options.resources || [] }; return PolicyPackManifestSchema.parse(manifest); } //#endregion //#region src/utils/id-utils.mjs const { blankNode, namedNode: namedNode$1 } = DataFactory; /** * Generate a UUID v4 * @returns {string} UUID v4 string */ const generateUUID = () => { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); }; /** * Generate a generic ID with optional prefix * @param {string} [prefix="id"] - Prefix for the ID * @returns {string} Generated ID */ const generateId = (prefix = "id") => { const uuid = generateUUID(); return `${prefix}-${uuid}`; }; /** * Generate a hash-based ID from input * @param {string} input - Input string to hash * @returns {string} Hash-based ID */ const generateHashId = (input) => { const hash = createHash("sha256"); hash.update(input); return hash.digest("hex"); }; //#endregion //#region src/utils/namespace-utils.mjs const { namedNode } = DataFactory; /** * Common RDF vocabularies and their namespaces */ const COMMON_VOCABULARIES = { RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", RDFS: "http://www.w3.org/2000/01/rdf-schema#", OWL: "http://www.w3.org/2002/07/owl#", XSD: "http://www.w3.org/2001/XMLSchema#", DC: "http://purl.org/dc/elements/1.1/", DCTERMS: "http://purl.org/dc/terms/", FOAF: "http://xmlns.com/foaf/0.1/", SKOS: "http://www.w3.org/2004/02/skos/core#", SCHEMA: "https://schema.org/", PROV: "http://www.w3.org/ns/prov#", SHACL: "http://www.w3.org/ns/shacl#", TIME: "http://www.w3.org/2006/time#", GEO: "http://www.opengis.net/ont/geosparql#", WGS84: "http://www.w3.org/2003/01/geo/wgs84_pos#", CC: "http://creativecommons.org/ns#", DOAP: "http://usefulinc.com/ns/doap#", VCARD: "http://www.w3.org/2006/vcard/ns#" }; /** * Common prefixes for serialization */ const COMMON_PREFIXES = { rdf: COMMON_VOCABULARIES.RDF, rdfs: COMMON_VOCABULARIES.RDFS, owl: COMMON_VOCABULARIES.OWL, xsd: COMMON_VOCABULARIES.XSD, dc: COMMON_VOCABULARIES.DC, dcterms: COMMON_VOCABULARIES.DCTERMS, foaf: COMMON_VOCABULARIES.FOAF, skos: COMMON_VOCABULARIES.SKOS, schema: COMMON_VOCABULARIES.SCHEMA, prov: COMMON_VOCABULARIES.PROV, sh: COMMON_VOCABULARIES.SHACL, time: COMMON_VOCABULARIES.TIME, geo: COMMON_VOCABULARIES.GEO, wgs84: COMMON_VOCABULARIES.WGS84, cc: COMMON_VOCABULARIES.CC, doap: COMMON_VOCABULARIES.DOAP, vcard: COMMON_VOCABULARIES.VCARD }; /** * Expand a CURIE to full IRI * @param {string} curie - CURIE to expand (e.g., "foaf:Person") * @param {Object} prefixes - Prefix mappings * @returns {string} Full IRI */ const expandCurie = (curie, prefixes) => { const colonIndex = curie.indexOf(":"); if (colonIndex === -1) return curie; const prefix = curie.slice(0, Math.max(0, colonIndex)); const localName = curie.slice(Math.max(0, colonIndex + 1)); if (prefixes[prefix]) return `${prefixes[prefix]}${localName}`; return curie; }; /** * Shrink a full IRI to CURIE if possible * @param {string} iri - Full IRI to shrink * @param {Object} prefixes - Prefix mappings * @returns {string} CURIE or original IRI if no match */ const shrinkIri = (iri, prefixes) => { for (const [prefix, namespace] of Object.entries(prefixes)) if (iri.startsWith(namespace)) { const localName = iri.slice(namespace.length); return `${prefix}:${localName}`; } return iri; }; //#endregion //#region src/composables/index.mjs const useValidator = () => { throw new Error("useValidator not implemented yet"); }; const usePrefixes = () => { throw new Error("usePrefixes not implemented yet"); }; //#endregion //#region src/utils/storage-utils.mjs const __dirname$1 = dirname(fileURLToPath(import.meta.url)); /** * Hook storage manager */ var HookStorage = class { constructor(storagePath = "./.unrdf") { this.storagePath = storagePath; this.hooksPath = join(storagePath, "hooks"); this.receiptsPath = join(storagePath, "receipts"); this.baselinesPath = join(storagePath, "baselines"); } /** * Initialize storage directories */ async init() { const dirs = [ this.storagePath, this.hooksPath, this.receiptsPath, this.baselinesPath ]; for (const dir of dirs) try { await access(dir); } catch { await mkdir(dir, { recursive: true }); } } /** * Save a hook definition * @param {Object} hook - Hook definition * @returns {Promise<string>} Hook ID */ async saveHook(hook) { await this.init(); const hookId = hook.id; const fileName = `${hookId.replace(/[^a-zA-Z0-9-_]/g, "_")}.json`; const filePath = join(this.hooksPath, fileName); const hookWithMeta = { ...hook, _metadata: { created: new Date().toISOString(), updated: new Date().toISOString(), version: "1.0.0" } }; await writeFile(filePath, JSON.stringify(hookWithMeta, null, 2)); return hookId; } /** * Load a hook definition * @param {string} hookId - Hook ID * @returns {Promise<Object|null>} Hook definition or null if not found */ async loadHook(hookId) { await this.init(); const fileName = `${hookId.replace(/[^a-zA-Z0-9-_]/g, "_")}.json`; const filePath = join(this.hooksPath, fileName); try { const content = await readFile(filePath, "utf-8"); const hookWithMeta = JSON.parse(content); const { _metadata,...hook } = hookWithMeta; return hook; } catch { return null; } } /** * List all stored hooks * @returns {Promise<Array<Object>>} Array of hook summaries */ async listHooks() { await this.init(); try { const files = await readdir(this.hooksPath); const hooks = []; for (const file of files) if (file.endsWith(".json")) try { const filePath = join(this.hooksPath, file); const content = await readFile(filePath, "utf-8"); const hookWithMeta = JSON.parse(content); hooks.push({ id: hookWithMeta.id, name: hookWithMeta.name, description: hookWithMeta.description, created: hookWithMeta._metadata?.created, updated: hookWithMeta._metadata?.updated, predicateCount: hookWithMeta.predicates?.length || 0 }); } catch {} return hooks.sort((a, b) => new Date(b.updated) - new Date(a.updated)); } catch { return []; } } /** * Delete a hook * @param {string} hookId - Hook ID * @returns {Promise<boolean>} True if deleted successfully */ async deleteHook(hookId) { await this.init(); const fileName = `${hookId.replace(/[^a-zA-Z0-9-_]/g, "_")}.json`; const filePath = join(this.hooksPath, fileName); try { await access(filePath); const deletedPath = join(this.hooksPath, `${fileName}.deleted`); await writeFile(deletedPath, ""); return true; } catch { return false; } } /** * Save a receipt * @param {Object} receipt - Receipt object * @returns {Promise<string>} Receipt ID */ async saveReceipt(receipt) { await this.init(); const receiptId = `${receipt.hookId}_${Date.now()}_${receipt.provenance.queryHash.substring(0, 8)}`; const fileName = `${receiptId}.json`; const filePath = join(this.receiptsPath, fileName); const receiptWithMeta = { ...receipt, _metadata: { saved: new Date().toISOString(), receiptId } }; await writeFile(filePath, JSON.stringify(receiptWithMeta, null, 2)); return receiptId; } /** * Load receipts for a hook * @param {string} hookId - Hook ID * @param {number} [limit=100] - Maximum number of receipts to return * @returns {Promise<Array<Object>>} Array of receipts */ async loadReceipts(hookId, limit = 100) { await this.init(); try { const files = await readdir(this.receiptsPath); const receipts = []; for (const file of files) if (file.endsWith(".json") && file.startsWith(hookId.replace(/[^a-zA-Z0-9-_]/g, "_"))) try { const filePath = join(this.receiptsPath, file); const content = await readFile(filePath, "utf-8"); const receiptWithMeta = JSON.parse(content); const { _metadata,...receipt } = receiptWithMeta; receipts.push(receipt); } catch {} return receipts.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0, limit); } catch { return []; } } /** * Save baseline data for a hook * @param {string} hookId - Hook ID * @param {Object} baseline - Baseline data * @returns {Promise<void>} */ async saveBaseline(hookId, baseline) { await this.init(); const fileName = `${hookId.replace(/[^a-zA-Z0-9-_]/g, "_")}_baseline.json`; const filePath = join(this.baselinesPath, fileName); const baselineWithMeta = { ...baseline, _metadata: { hookId, saved: new Date().toISOString() } }; await writeFile(filePath, JSON.stringify(baselineWithMeta, null, 2)); } /** * Load baseline data for a hook * @param {string} hookId - Hook ID * @returns {Promise<Object|null>} Baseline data or null if not found */ async loadBaseline(hookId) { await this.init(); const fileName = `${hookId.replace(/[^a-zA-Z0-9-_]/g, "_")}_baseline.json`; const filePath = join(this.baselinesPath, fileName); try { const content = await readFile(filePath, "utf-8"); const baselineWithMeta = JSON.parse(content); const { _metadata,...baseline } = baselineWithMeta; return baseline; } catch { return null; } } /** * Get storage statistics * @returns {Promise<Object>} Storage statistics */ async getStats() { await this.init(); try { const [hookFiles, receiptFiles, baselineFiles] = await Promise.all([ readdir(this.hooksPath), readdir(this.receiptsPath), readdir(this.baselinesPath) ]); const validHooks = hookFiles.filter((f) => f.endsWith(".json") && !f.endsWith(".deleted.json")).length; const receipts = receiptFiles.filter((f) => f.endsWith(".json")).length; const baselines = baselineFiles.filter((f) => f.endsWith(".json")).length; return { hooks: validHooks, receipts, baselines, totalFiles: hookFiles.length + receiptFiles.length + baselineFiles.length }; } catch { return { hooks: 0, receipts: 0, baselines: 0, totalFiles: 0 }; } } }; /** * Default storage instance */ const defaultStorage = new HookStorage(); /** * Storage configuration */ const storageConfig = { defaultPath: "./.unrdf", maxReceiptsPerHook: 1e3, cleanupInterval: 24 * 60 * 60 * 1e3, retentionDays: 30 }; //#endregion //#region src/cli.mjs const __dirname = dirname(fileURLToPath(import.meta.url)); /** * Load configuration from unrdf.config.mjs if it exists * @returns {Promise<Object>} Configuration object */ async function loadConfig() { const configPath = resolve(process.cwd(), "unrdf.config.mjs"); try { await access(configPath); const config = await import(configPath); return config.default || {}; } catch { return { baseIRI: process.env.UNRDF_BASE_IRI || "http://example.org/", prefixes: process.env.UNRDF_PREFIXES ? JSON.parse(process.env.UNRDF_PREFIXES) : { "ex": "http://example.org/", "foaf": "http://xmlns.com/foaf/0.1/", "schema": "https://schema.org/", "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdfs": "http://www.w3.org/2000/01/rdf-schema#", "owl": "http://www.w3.org/2002/07/owl#" }, validation: { strict: true, validateOnLoad: true } }; } } const main = defineCommand({ meta: { name: "unrdf", version: "1.0.0", description: "UNRDF - Opinionated composable framework for RDF knowledge operations" }, subCommands: { parse: defineCommand({ meta: { name: "parse", description: "Parse RDF data from various formats" }, args: { input: { type: "positional", description: "Input file path", required: true } }, async run(ctx) { try { console.log("🔄 Parsing RDF data..."); const config = await loadConfig(); const runApp = initStore([], { baseIRI: config.baseIRI }); await runApp(async () => { const store = useStoreContext(); const turtle = await useTurtle(); let inputData; try { inputData = await readFile(ctx.args.input, "utf-8"); } catch (error) { console.error(`❌ File not found: ${ctx.args.input}`); process.exit(1); } let quads; const startTime = Date.now(); const format = ctx.args.format || "turtle"; switch (format) { case "turtle": quads = await turtle.parse(inputData); break; case "n-quads": console.error("❌ N-Quads parsing not yet implemented"); process.exit(1); break; default: console.error(`❌ Unsupported format: ${format}`); process.exit(1); } store.add(...quads); const endTime = Date.now(); const duration = endTime - startTime; console.log(`✅ Parsed ${quads.length} triples successfully in ${duration}ms`); if (ctx.args.output) { const serialized = await turtle.serialize(); await writeFile(ctx.args.output, serialized); console.log(`📄 Output written to ${ctx.args.output}`); } }); } catch (error) { console.error(`❌ Parse error: ${error.message}`); process.exit(1); } } }), query: defineCommand({ meta: { name: "query", description: "Query RDF data with SPARQL" }, args: { input: { type: "positional", description: "Input file path", required: true } }, async run(ctx) { try { console.log("🔍 Executing SPARQL query..."); const config = await loadConfig(); const runApp = initStore([], { baseIRI: config.baseIRI }); await runApp(async () => { const store = useStoreContext(); const graph = useGraph(); const turtle = await useTurtle(); const inputData = await readFile(ctx.args.input, "utf-8"); const quads = await turtle.parse(inputData); store.add(...quads); let query; if (ctx.args.query) query = ctx.args.query; else if (ctx.args["query-file"]) query = await readFile(ctx.args["query-file"], "utf-8"); else { console.error("❌ Query required: use --query or --query-file"); process.exit(1); } const results = await graph.select(query); let output; const format = ctx.args.format || "table"; switch (format) { case "json": output = JSON.stringify(results, null, 2); break; case "csv": if (results.length === 0) output = ""; else { const headers = Object.keys(results[0]); output = headers.join(",") + "\n" + results.map((row) => headers.map((h) => row[h] || "").join(",")).join("\n"); } break; case "table": default: if (results.length === 0) output = "No results found."; else { const headers = Object.keys(results[0]); const colWidths = headers.map((h) => Math.max(h.length, ...results.map((r) => String(r[h] || "").length))); output = headers.map((h, i) => h.padEnd(colWidths[i])).join(" | ") + "\n" + headers.map((_, i) => "-".repeat(colWidths[i])).join("-+-") + "\n" + results.map((row) => headers.map((h, i) => String(row[h] || "").padEnd(colWidths[i])).join(" | ")).join("\n"); } break; } console.log(output); if (ctx.args.output) { await writeFile(ctx.args.output, output); console.log(`📄 Results written to ${ctx.args.output}`); } }); } catch (error) { console.error(`❌ Query error: ${error.message}`); process.exit(1); } } }), validate: defineCommand({ meta: { name: "validate", description: "Validate RDF data against SHACL shapes" }, args: { data: { type: "positional", description: "Data file path", required: true } }, async run(ctx) { try { console.log("✅ Validating RDF data..."); const config = await loadConfig(); const runApp = initStore([], { baseIRI: config.baseIRI }); await runApp(async () => { const store = useStoreContext(); const turtle = await useTurtle(); const validator = await useValidator(); const dataContent = await readFile(ctx.args.data, "utf-8"); const dataQuads = await turtle.parse(dataContent); store.add(...dataQuads); const shapeContent = await readFile(ctx.args.shape, "utf-8"); const shapeQuads = await turtle.parse(shapeContent); const report = await validator.validate(store.store, shapeQuads); if (report.conforms) console.log("✅ Validation passed"); else { console.log("❌ Validation failed"); console.log(`Found ${report.results.length} violations:`); for (const result of report.results) console.log(` - ${result.message} (${result.severity})`); } if (ctx.args.output) { await writeFile(ctx.args.output, JSON.stringify(report, null, 2)); console.log(`📄 Validation report written to ${ctx.args.output}`); } }); } catch (error) { console.error(`❌ Validation error: ${error.message}`); process.exit(1); } } }), convert: defineCommand({ meta: { name: "convert", description: "Convert between RDF and structured data formats" }, args: { input: { type: "positional", description: "Input file path", required: true } }, async run(ctx) { try { console.log("🔄 Converting data..."); const config = await loadConfig(); const runApp = initStore([], { baseIRI: config.baseIRI }); await runApp(async () => { const store = useStoreContext(); const turtle = await useTurtle(); const inputData = await readFile(ctx.args.input, "utf-8"); let quads; co