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
JavaScript
#!/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