UNPKG

meta-log-db

Version:

Native database package for Meta-Log (ProLog, DataLog, R5RS)

1,598 lines (1,587 loc) 204 kB
/** * Unification algorithm for ProLog */ class Unification { /** * Unify two terms */ static unify(term1, term2, bindings = new Map()) { // If terms are identical if (term1 === term2) { return bindings; } // If term1 is a variable if (this.isVariable(term1)) { const value = bindings.get(term1); if (value !== undefined) { return this.unify(value, term2, bindings); } bindings.set(term1, term2); return bindings; } // If term2 is a variable if (this.isVariable(term2)) { const value = bindings.get(term2); if (value !== undefined) { return this.unify(term1, value, bindings); } bindings.set(term2, term1); return bindings; } // If both are arrays (compound terms) if (Array.isArray(term1) && Array.isArray(term2)) { if (term1.length !== term2.length) { return null; } for (let i = 0; i < term1.length; i++) { const result = this.unify(term1[i], term2[i], bindings); if (result === null) { return null; } bindings = result; } return bindings; } // If both are objects if (typeof term1 === 'object' && typeof term2 === 'object' && term1 !== null && term2 !== null) { const keys1 = Object.keys(term1); const keys2 = Object.keys(term2); if (keys1.length !== keys2.length) { return null; } for (const key of keys1) { if (!keys2.includes(key)) { return null; } const result = this.unify(term1[key], term2[key], bindings); if (result === null) { return null; } bindings = result; } return bindings; } // Terms don't unify return null; } /** * Check if term is a variable (starts with ?) */ static isVariable(term) { return typeof term === 'string' && term.startsWith('?'); } /** * Apply bindings to a term */ static applyBindings(term, bindings) { if (this.isVariable(term)) { return bindings.get(term) || term; } if (Array.isArray(term)) { return term.map(t => this.applyBindings(t, bindings)); } if (typeof term === 'object' && term !== null) { const result = {}; for (const [key, value] of Object.entries(term)) { result[key] = this.applyBindings(value, bindings); } return result; } return term; } /** * Merge bindings */ static mergeBindings(bindings1, bindings2) { const merged = new Map(bindings1); for (const [key, value] of bindings2) { const existing = merged.get(key); if (existing !== undefined) { const unified = this.unify(existing, value, new Map(merged)); if (unified === null) { return null; } merged.set(key, unified.get(key) || value); } else { merged.set(key, value); } } return merged; } } /** * SLD Resolution for ProLog */ class Resolution { /** * Resolve a goal against facts and rules */ static resolve(goal, facts, rules, bindings = new Map()) { const results = []; // Parse goal const goalTerm = this.parseTerm(goal); // Try to match against facts for (const fact of facts) { const factTerm = { predicate: fact.predicate, args: fact.args }; const unified = Unification.unify(goalTerm, factTerm, new Map(bindings)); if (unified !== null) { results.push(unified); } } // Try to match against rules for (const rule of rules) { const ruleHead = this.parseTerm(rule.head); const unified = Unification.unify(goalTerm, ruleHead, new Map(bindings)); if (unified !== null) { // Resolve body goals const bodyResults = this.resolveBody(rule.body, facts, rules, unified); results.push(...bodyResults); } } return results; } /** * Resolve body goals */ static resolveBody(body, facts, rules, bindings) { if (body.length === 0) { return [bindings]; } const [firstGoal, ...restGoals] = body; const firstResults = this.resolve(firstGoal, facts, rules, bindings); const allResults = []; for (const result of firstResults) { const restResults = this.resolveBody(restGoals, facts, rules, result); allResults.push(...restResults); } return allResults; } /** * Parse a ProLog term string into structured format */ static parseTerm(termStr) { const trimmed = termStr.trim(); // Simple predicate(args) format const match = trimmed.match(/^(\w+)\((.*)\)$/); if (match) { const predicate = match[1]; const argsStr = match[2]; const args = this.parseArgs(argsStr); return { predicate, args }; } // Simple predicate format if (/^\w+$/.test(trimmed)) { return { predicate: trimmed, args: [] }; } // Variable if (trimmed.startsWith('?')) { return trimmed; } // Fallback: treat as string return { predicate: trimmed, args: [] }; } /** * Parse arguments from string */ static parseArgs(argsStr) { if (!argsStr.trim()) { return []; } const args = []; let current = ''; let depth = 0; for (const char of argsStr) { if (char === '(') { depth++; current += char; } else if (char === ')') { depth--; current += char; } else if (char === ',' && depth === 0) { args.push(this.parseArg(current.trim())); current = ''; } else { current += char; } } if (current.trim()) { args.push(this.parseArg(current.trim())); } return args; } /** * Parse a single argument */ static parseArg(argStr) { const trimmed = argStr.trim(); // Variable if (trimmed.startsWith('?')) { return trimmed; } // Number if (/^-?\d+$/.test(trimmed)) { return parseInt(trimmed, 10); } if (/^-?\d+\.\d+$/.test(trimmed)) { return parseFloat(trimmed); } // String (quoted) if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1); } // Compound term if (trimmed.includes('(')) { return this.parseTerm(trimmed); } // Atom return trimmed; } } /** * ProLog Engine for Meta-Log Database */ class PrologEngine { constructor() { this.facts = []; this.rules = []; } /** * Add facts to the database */ addFacts(facts) { this.facts.push(...facts); } /** * Add a rule to the database */ addRule(rule) { this.rules.push(rule); } /** * Build database from facts */ buildDb(facts) { this.facts = [...facts]; } /** * Query the database */ async query(goal) { const results = Resolution.resolve(goal, this.facts, this.rules); // Convert results to bindings format const bindings = []; for (const resultBindings of results) { const binding = {}; for (const [key, value] of resultBindings) { binding[key] = value; } bindings.push(binding); } return { bindings }; } /** * Get all facts */ getFacts() { return [...this.facts]; } /** * Get all rules */ getRules() { return [...this.rules]; } /** * Clear database */ clear() { this.facts = []; this.rules = []; } } /** * Fixed-point computation for DataLog */ class FixedPoint { /** * Compute fixed point of a DataLog program */ static compute(program) { let facts = new Set(this.factsToStrings(program.facts)); let previousSize = 0; let iterations = 0; const maxIterations = 1000; while (facts.size !== previousSize && iterations < maxIterations) { previousSize = facts.size; // Apply all rules for (const rule of program.rules) { const newFacts = this.applyRule(rule, Array.from(facts).map(s => this.stringToFact(s))); for (const fact of newFacts) { facts.add(this.factToString(fact)); } } iterations++; } return Array.from(facts).map(s => this.stringToFact(s)); } /** * Apply a rule to generate new facts */ static applyRule(rule, facts) { const newFacts = []; // Match body predicates against facts const bodyMatches = this.matchBody(rule.body, facts); // Generate head facts for each match for (const match of bodyMatches) { const headFact = this.instantiateHead(rule.head, match); if (headFact) { newFacts.push(headFact); } } return newFacts; } /** * Match body predicates against facts */ static matchBody(body, facts) { if (body.length === 0) { return [new Map()]; } const [firstPred, ...restPreds] = body; const firstMatches = this.matchPredicate(firstPred, facts); const allMatches = []; for (const match of firstMatches) { const restMatches = this.matchBody(restPreds, facts); for (const restMatch of restMatches) { const merged = this.mergeMatches(match, restMatch); if (merged) { allMatches.push(merged); } } } return allMatches; } /** * Match a predicate against facts */ static matchPredicate(predicate, facts) { const matches = []; const parsed = this.parsePredicate(predicate); for (const fact of facts) { if (fact.predicate === parsed.predicate) { const match = this.matchArgs(parsed.args, fact.args); if (match) { matches.push(match); } } } return matches; } /** * Match arguments (with variable binding) */ static matchArgs(patternArgs, factArgs) { if (patternArgs.length !== factArgs.length) { return null; } const bindings = new Map(); for (let i = 0; i < patternArgs.length; i++) { const pattern = patternArgs[i]; const fact = factArgs[i]; if (pattern.startsWith('?')) { // Variable const existing = bindings.get(pattern); if (existing !== undefined && existing !== fact) { return null; } bindings.set(pattern, fact); } else if (pattern !== fact.toString()) { // Constant doesn't match return null; } } return bindings; } /** * Instantiate head with bindings */ static instantiateHead(head, bindings) { const parsed = this.parsePredicate(head); const args = []; for (const arg of parsed.args) { if (arg.startsWith('?')) { const value = bindings.get(arg); if (value === undefined) { return null; } args.push(value); } else { args.push(arg); } } return { predicate: parsed.predicate, args }; } /** * Parse predicate string */ static parsePredicate(predStr) { const match = predStr.match(/^(\w+)\((.*)\)$/); if (match) { const predicate = match[1]; const argsStr = match[2]; const args = argsStr ? argsStr.split(',').map(s => s.trim()) : []; return { predicate, args }; } return { predicate: predStr, args: [] }; } /** * Merge two match bindings */ static mergeMatches(match1, match2) { const merged = new Map(match1); for (const [key, value] of match2) { const existing = merged.get(key); if (existing !== undefined && existing !== value) { return null; } merged.set(key, value); } return merged; } /** * Convert fact to string for set operations */ static factToString(fact) { return `${fact.predicate}(${fact.args.join(',')})`; } /** * Convert facts to strings */ static factsToStrings(facts) { return facts.map(f => this.factToString(f)); } /** * Convert string back to fact */ static stringToFact(str) { const match = str.match(/^(\w+)\((.*)\)$/); if (match) { const predicate = match[1]; const args = match[2] ? match[2].split(',').map(s => s.trim()) : []; return { predicate, args }; } return { predicate: str, args: [] }; } } /** * Fact extraction utilities for DataLog */ class FactExtraction { /** * Extract facts from JSONL canvas objects */ static extractFromCanvas(objects) { const facts = []; for (const obj of objects) { // Node facts if (obj.type === 'node' || obj.id) { facts.push({ predicate: 'node', args: [ obj.id || obj._id || 'unknown', obj.type || 'unknown', obj.x || 0, obj.y || 0, obj.text || obj.label || '' ] }); // Extract properties for (const [key, value] of Object.entries(obj)) { if (!['id', '_id', 'type', 'x', 'y', 'text', 'label'].includes(key)) { facts.push({ predicate: `has_${key}`, args: [obj.id || obj._id, value] }); } } } // Edge facts if (obj.type === 'edge' || obj.fromNode || obj.toNode) { facts.push({ predicate: 'edge', args: [ obj.id || obj._id || 'unknown', obj.type || 'unknown', obj.fromNode || obj.from || 'unknown', obj.toNode || obj.to || 'unknown' ] }); // Vertical/horizontal relationships if (obj.type?.startsWith('v:') || obj.type === 'vertical') { facts.push({ predicate: 'vertical', args: [obj.fromNode || obj.from, obj.toNode || obj.to] }); } if (obj.type?.startsWith('h:') || obj.type === 'horizontal') { facts.push({ predicate: 'horizontal', args: [obj.fromNode || obj.from, obj.toNode || obj.to] }); } } } return facts; } /** * Extract facts from structured data */ static extractFromData(data) { const facts = []; if (Array.isArray(data)) { for (const item of data) { facts.push(...this.extractFromData(item)); } } else if (typeof data === 'object' && data !== null) { // Extract object properties as facts for (const [key, value] of Object.entries(data)) { if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { facts.push({ predicate: key, args: [value] }); } else if (Array.isArray(value)) { for (const item of value) { facts.push({ predicate: key, args: [item] }); } } } } return facts; } } /** * DataLog Engine for Meta-Log Database */ class DatalogEngine { constructor() { this.facts = []; this.rules = []; } /** * Add facts to the database */ addFacts(facts) { this.facts.push(...facts); } /** * Add a rule to the database */ addRule(rule) { this.rules.push(rule); } /** * Build DataLog program from rules */ buildProgram(rules) { return { rules: [...rules], facts: [...this.facts] }; } /** * Query the database */ async query(goal, program) { const targetProgram = program || this.buildProgram(this.rules); // Compute fixed point const allFacts = FixedPoint.compute(targetProgram); // Match goal against facts const parsedGoal = this.parseGoal(goal); const matchingFacts = allFacts.filter(fact => { if (fact.predicate !== parsedGoal.predicate) { return false; } if (fact.args.length !== parsedGoal.args.length) { return false; } // Match arguments (variables match anything) for (let i = 0; i < fact.args.length; i++) { const goalArg = parsedGoal.args[i]; if (!goalArg.startsWith('?') && goalArg !== fact.args[i].toString()) { return false; } } return true; }); return { facts: matchingFacts }; } /** * Compute fixed point */ fixedPoint(program) { return FixedPoint.compute(program); } /** * Extract facts from canvas objects */ extractFacts(objects) { return FactExtraction.extractFromCanvas(objects); } /** * Parse goal string */ parseGoal(goal) { const match = goal.match(/^(\w+)\((.*)\)$/); if (match) { const predicate = match[1]; const argsStr = match[2]; const args = argsStr ? argsStr.split(',').map(s => s.trim()) : []; return { predicate, args }; } return { predicate: goal, args: [] }; } /** * Get all facts */ getFacts() { return [...this.facts]; } /** * Get all rules */ getRules() { return [...this.rules]; } /** * Clear database */ clear() { this.facts = []; this.rules = []; } } /** * Enhanced SPARQL Query Parser * Supports: SELECT, DISTINCT, ORDER BY, LIMIT, OFFSET, FILTER, OPTIONAL, UNION */ /** * Parse SPARQL query string into structured query object */ class SparqlParser { /** * Parse SPARQL query */ static parse(query) { const normalized = this.normalizeQuery(query); // Parse query type const queryType = this.parseQueryType(normalized); // Parse SELECT clause const selectMatch = normalized.match(/SELECT\s+(DISTINCT\s+)?(.*?)\s+WHERE/i); if (!selectMatch) { throw new Error('Invalid SPARQL SELECT query'); } const distinct = !!selectMatch[1]; const variablesStr = selectMatch[2].trim(); const variables = this.parseVariables(variablesStr); // Parse WHERE clause const whereMatch = normalized.match(/WHERE\s*\{([^}]+)\}/is); if (!whereMatch) { throw new Error('Invalid SPARQL WHERE clause'); } const whereClause = whereMatch[1]; const patterns = this.parsePatterns(whereClause); const filters = this.parseFilters(whereClause); const optional = this.parseOptional(whereClause); // Parse ORDER BY const orderByMatch = normalized.match(/ORDER\s+BY\s+(.*?)(?:\s+LIMIT|\s+OFFSET|$)/i); const orderBy = orderByMatch ? this.parseOrderBy(orderByMatch[1]) : undefined; // Parse LIMIT const limitMatch = normalized.match(/LIMIT\s+(\d+)/i); const limit = limitMatch ? parseInt(limitMatch[1], 10) : undefined; // Parse OFFSET const offsetMatch = normalized.match(/OFFSET\s+(\d+)/i); const offset = offsetMatch ? parseInt(offsetMatch[1], 10) : undefined; return { type: queryType, distinct, variables, where: patterns, optional, filters, orderBy, limit, offset }; } /** * Normalize query string */ static normalizeQuery(query) { return query .replace(/\s+/g, ' ') .replace(/\s*\{\s*/g, ' { ') .replace(/\s*\}\s*/g, ' } ') .trim(); } /** * Parse query type */ static parseQueryType(query) { if (query.match(/^\s*SELECT/i)) return 'SELECT'; if (query.match(/^\s*ASK/i)) return 'ASK'; if (query.match(/^\s*CONSTRUCT/i)) return 'CONSTRUCT'; if (query.match(/^\s*DESCRIBE/i)) return 'DESCRIBE'; return 'SELECT'; // Default } /** * Parse variables from SELECT clause */ static parseVariables(variablesStr) { if (variablesStr === '*') { return ['*']; } return variablesStr .split(/\s+/) .filter(v => v.trim()) .map(v => v.trim()); } /** * Parse triple patterns from WHERE clause */ static parsePatterns(whereClause) { const patterns = []; const lines = whereClause.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('FILTER') && !l.startsWith('OPTIONAL')); for (const line of lines) { const match = line.match(/(\S+)\s+(\S+)\s+(\S+)\s*\.?/); if (match) { patterns.push({ subject: match[1], predicate: match[2], object: match[3] }); } } return patterns; } /** * Parse OPTIONAL patterns */ static parseOptional(whereClause) { const optionalMatch = whereClause.match(/OPTIONAL\s*\{([^}]+)\}/is); if (!optionalMatch) return undefined; return this.parsePatterns(optionalMatch[1]); } /** * Parse FILTER expressions */ static parseFilters(whereClause) { const filters = []; const filterMatches = whereClause.matchAll(/FILTER\s*\(([^)]+)\)/gi); for (const match of filterMatches) { const expression = match[1].trim(); // Parse different filter types if (expression.includes('=')) { const [left, right] = expression.split('=').map(s => s.trim()); filters.push({ expression, type: 'equals', left, right }); } else if (expression.includes('!=')) { const [left, right] = expression.split('!=').map(s => s.trim()); filters.push({ expression, type: 'notEquals', left, right }); } else if (expression.includes('>')) { const [left, right] = expression.split('>').map(s => s.trim()); filters.push({ expression, type: 'greaterThan', left, right }); } else if (expression.includes('<')) { const [left, right] = expression.split('<').map(s => s.trim()); filters.push({ expression, type: 'lessThan', left, right }); } else if (expression.includes('regex')) { filters.push({ expression, type: 'regex', left: expression }); } else if (expression.includes('bound')) { filters.push({ expression, type: 'bound', left: expression }); } else { filters.push({ expression, type: 'custom', left: expression }); } } return filters; } /** * Parse ORDER BY clause */ static parseOrderBy(orderByStr) { const orders = []; const parts = orderByStr.split(/\s+/); for (let i = 0; i < parts.length; i++) { const part = parts[i].trim(); if (part === 'ASC' || part === 'DESC') { if (i > 0) { orders[orders.length - 1].direction = part; } } else if (part && !part.match(/^(ASC|DESC)$/i)) { orders.push({ variable: part, direction: 'ASC' // Default }); } } return orders; } } /** * Enhanced SPARQL Query Executor * Executes parsed SPARQL queries with full feature support */ class SparqlExecutor { constructor(triples) { this.triples = triples; } /** * Execute SPARQL query */ async execute(query) { // Execute WHERE clause patterns let bindings = this.executePatterns(query.where); // Apply OPTIONAL patterns if (query.optional) { bindings = this.applyOptional(bindings, query.optional); } // Apply FILTER expressions if (query.filters && query.filters.length > 0) { bindings = this.applyFilters(bindings, query.filters); } // Apply DISTINCT if (query.distinct) { bindings = this.applyDistinct(bindings); } // Apply ORDER BY if (query.orderBy && query.orderBy.length > 0) { bindings = this.applyOrderBy(bindings, query.orderBy); } // Apply LIMIT and OFFSET if (query.offset) { bindings = bindings.slice(query.offset); } if (query.limit) { bindings = bindings.slice(0, query.limit); } // Project variables bindings = this.projectVariables(bindings, query.variables); return { results: { bindings: bindings.map(b => this.formatBinding(b)) } }; } /** * Execute triple patterns */ executePatterns(patterns) { if (patterns.length === 0) { return [{}]; } let bindings = [{}]; for (const pattern of patterns) { const newBindings = []; for (const binding of bindings) { const matches = this.matchPattern(pattern, binding); for (const match of matches) { newBindings.push({ ...binding, ...match }); } } bindings = newBindings; } return bindings; } /** * Match a pattern against triples with existing bindings */ matchPattern(pattern, existingBindings) { const subject = this.resolveValue(pattern.subject, existingBindings); const predicate = this.resolveValue(pattern.predicate, existingBindings); const object = this.resolveValue(pattern.object, existingBindings); const matches = []; const patternTriple = { subject: subject.startsWith('?') ? undefined : subject, predicate: predicate.startsWith('?') ? undefined : predicate, object: object.startsWith('?') ? undefined : object }; const matchingTriples = this.queryTriples(patternTriple); for (const triple of matchingTriples) { const newBinding = {}; if (subject.startsWith('?')) { newBinding[subject] = triple.subject; } if (predicate.startsWith('?')) { newBinding[predicate] = triple.predicate; } if (object.startsWith('?')) { const objValue = typeof triple.object === 'string' ? triple.object : triple.object.value; newBinding[object] = objValue; } // Check if binding is consistent with existing bindings let consistent = true; for (const [key, value] of Object.entries(newBinding)) { if (existingBindings[key] && existingBindings[key] !== value) { consistent = false; break; } } if (consistent) { matches.push(newBinding); } } return matches; } /** * Resolve variable or literal value */ resolveValue(value, bindings) { if (value.startsWith('?')) { return bindings[value] || value; } return value; } /** * Query triples by pattern */ queryTriples(pattern) { return this.triples.filter(triple => { if (pattern.subject && triple.subject !== pattern.subject) { return false; } if (pattern.predicate && triple.predicate !== pattern.predicate) { return false; } if (pattern.object) { const objStr = typeof triple.object === 'string' ? triple.object : triple.object.value; if (objStr !== pattern.object) { return false; } } return true; }); } /** * Apply OPTIONAL patterns */ applyOptional(bindings, optionalPatterns) { const result = []; for (const binding of bindings) { const optionalMatches = this.executePatterns(optionalPatterns); if (optionalMatches.length > 0) { for (const match of optionalMatches) { result.push({ ...binding, ...match }); } } else { result.push(binding); } } return result; } /** * Apply FILTER expressions */ applyFilters(bindings, filters) { return bindings.filter(binding => { return filters.every(filter => this.evaluateFilter(filter, binding)); }); } /** * Evaluate a filter expression */ evaluateFilter(filter, binding) { const leftValue = this.resolveValue(filter.left, binding); const rightValue = filter.right ? this.resolveValue(filter.right, binding) : undefined; switch (filter.type) { case 'equals': return leftValue === rightValue; case 'notEquals': return leftValue !== rightValue; case 'greaterThan': return parseFloat(leftValue) > parseFloat(rightValue || '0'); case 'lessThan': return parseFloat(leftValue) < parseFloat(rightValue || '0'); case 'bound': return binding[filter.left] !== undefined; case 'regex': // Simplified regex - full implementation would parse regex properly return true; default: return true; } } /** * Apply DISTINCT */ applyDistinct(bindings) { const seen = new Set(); const result = []; for (const binding of bindings) { const key = JSON.stringify(binding); if (!seen.has(key)) { seen.add(key); result.push(binding); } } return result; } /** * Apply ORDER BY */ applyOrderBy(bindings, orderBy) { return [...bindings].sort((a, b) => { for (const order of orderBy) { const aValue = a[order.variable] || ''; const bValue = b[order.variable] || ''; let comparison = 0; if (aValue < bValue) comparison = -1; else if (aValue > bValue) comparison = 1; if (order.direction === 'DESC') { comparison = -comparison; } if (comparison !== 0) { return comparison; } } return 0; }); } /** * Project variables (SELECT clause) */ projectVariables(bindings, variables) { if (variables.includes('*')) { return bindings; } return bindings.map(binding => { const projected = {}; for (const variable of variables) { if (binding[variable]) { projected[variable] = binding[variable]; } } return projected; }); } /** * Format binding for output */ formatBinding(binding) { const formatted = {}; for (const [key, value] of Object.entries(binding)) { formatted[key] = { value, type: this.inferType(value) }; } return formatted; } /** * Infer RDF type from value */ inferType(value) { if (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('<')) { return 'uri'; } if (value.startsWith('"') && value.endsWith('"')) { return 'literal'; } if (/^-?\d+$/.test(value)) { return 'typed-literal'; } return 'literal'; } } /** * RDF Triple Store */ class TripleStore { constructor() { this.triples = []; this.queryCache = new Map(); this.cacheEnabled = true; } /** * Add triples to the store */ addTriples(triples) { this.triples.push(...triples); } /** * Query triples by pattern */ query(pattern) { return this.triples.filter(triple => { if (pattern.subject && triple.subject !== pattern.subject) { return false; } if (pattern.predicate && triple.predicate !== pattern.predicate) { return false; } if (pattern.object) { const objStr = typeof triple.object === 'string' ? triple.object : triple.object.value; if (objStr !== pattern.object) { return false; } } return true; }); } /** * Execute SPARQL query (enhanced implementation) * Supports: SELECT, DISTINCT, ORDER BY, LIMIT, OFFSET, FILTER, OPTIONAL */ async sparql(query) { // Check cache if (this.cacheEnabled) { const cached = this.queryCache.get(query); if (cached) { return cached; } } try { // Parse query using enhanced parser const parsedQuery = SparqlParser.parse(query); // Execute query using enhanced executor const executor = new SparqlExecutor(this.triples); const result = await executor.execute(parsedQuery); // Cache result if (this.cacheEnabled) { this.queryCache.set(query, result); } return result; } catch (error) { // Fallback to simplified parser for backward compatibility return this.sparqlSimple(query); } } /** * Simple SPARQL query execution (backward compatibility) */ async sparqlSimple(query) { // Simple SELECT query parser const selectMatch = query.match(/SELECT\s+(.*?)\s+WHERE/i); if (!selectMatch) { throw new Error('Unsupported SPARQL query format'); } selectMatch[1].split(/\s+/).filter(v => v.startsWith('?')); // Extract WHERE clause patterns const whereMatch = query.match(/WHERE\s*\{([^}]+)\}/is); if (!whereMatch) { return { results: { bindings: [] } }; } const patterns = this.parseSparqlPatterns(whereMatch[1]); const bindings = []; // Simple pattern matching for (const pattern of patterns) { const matches = this.query(pattern); for (const match of matches) { const binding = {}; if (pattern.subject?.startsWith('?')) { binding[pattern.subject] = { value: match.subject, type: 'uri' }; } if (pattern.predicate?.startsWith('?')) { binding[pattern.predicate] = { value: match.predicate, type: 'uri' }; } if (pattern.object?.startsWith('?')) { const objValue = typeof match.object === 'string' ? match.object : match.object.value; binding[pattern.object] = { value: objValue, type: 'uri' }; } bindings.push(binding); } } return { results: { bindings } }; } /** * Enable/disable query caching */ setCacheEnabled(enabled) { this.cacheEnabled = enabled; if (!enabled) { this.queryCache.clear(); } } /** * Clear query cache */ clearCache() { this.queryCache.clear(); } /** * Parse SPARQL patterns from WHERE clause */ parseSparqlPatterns(whereClause) { const patterns = []; const lines = whereClause.split('\n').map(l => l.trim()).filter(l => l); for (const line of lines) { const match = line.match(/(\S+)\s+(\S+)\s+(\S+)\s*\./); if (match) { patterns.push({ subject: match[1], predicate: match[2], object: match[3] }); } } return patterns; } /** * RDFS entailment (simplified) */ rdfsEntailment(triples) { const entailed = [...triples]; const rdfType = '<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>'; const rdfsSubClassOf = '<http://www.w3.org/2000/01/rdf-schema#subClassOf>'; // Find subClassOf relationships const subClassTriples = triples.filter(t => t.predicate === rdfsSubClassOf); // Apply subClassOf transitivity for (const subClassTriple of subClassTriples) { const subClass = subClassTriple.subject; const superClass = typeof subClassTriple.object === 'string' ? subClassTriple.object : subClassTriple.object.value; // Find all instances of subClass const instances = triples.filter(t => t.predicate === rdfType && (typeof t.object === 'string' ? t.object : t.object.value) === subClass); // Add type assertions for superClass for (const instance of instances) { entailed.push({ subject: instance.subject, predicate: rdfType, object: superClass }); } } return entailed; } /** * Get all triples */ getTriples() { return [...this.triples]; } /** * Clear all triples */ clear() { this.triples = []; } } /** * Turtle/RDF Parser for SHACL Shapes * Parses Turtle format RDF files into structured data */ /** * Turtle Parser * Basic Turtle parser for SHACL shape files */ class TurtleParser { /** * Parse Turtle content into triples */ static parse(content) { const triples = []; const lines = content.split('\n'); let inMultiLine = false; let multiLineValue = ''; for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Skip empty lines and comments if (!line || line.startsWith('#')) { continue; } // Handle multi-line strings if (inMultiLine) { multiLineValue += ' ' + line; if (line.endsWith('"""') || line.endsWith("'''")) { inMultiLine = false; line = multiLineValue; multiLineValue = ''; } else { continue; } } // Check for multi-line string start if (line.includes('"""') || line.includes("'''")) { inMultiLine = true; multiLineValue = line; continue; } // Parse prefixes if (line.startsWith('@prefix') || line.startsWith('PREFIX')) { // Prefix handling would go here continue; } // Parse triples const parts = this.splitTripleLine(line); if (parts.length >= 3) { const subject = this.expandIRI(parts[0]); const predicate = this.expandIRI(parts[1]); const object = this.parseObject(parts[2]); triples.push({ subject, predicate, object }); // Handle predicate lists (a b c .) if (parts.length > 3) { for (let j = 2; j < parts.length - 1; j++) { const nextPredicate = this.expandIRI(parts[j]); const nextObject = this.parseObject(parts[j + 1]); triples.push({ subject, predicate: nextPredicate, object: nextObject }); } } } } return triples; } /** * Split a triple line into parts */ static splitTripleLine(line) { const parts = []; let current = ''; let inQuotes = false; let quoteChar = ''; let inBrackets = 0; for (let i = 0; i < line.length; i++) { const char = line[i]; i < line.length - 1 ? line[i + 1] : ''; if (!inQuotes && !inBrackets && (char === ' ' || char === '\t')) { if (current.trim()) { parts.push(current.trim()); current = ''; } continue; } if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; quoteChar = char; current += char; } else if (char === quoteChar && inQuotes) { inQuotes = false; quoteChar = ''; current += char; } else if (char === '<' && !inQuotes) { inBrackets++; current += char; } else if (char === '>' && !inQuotes) { inBrackets--; current += char; } else { current += char; } } if (current.trim()) { parts.push(current.trim()); } return parts.filter(p => p && p !== '.'); } /** * Parse object value */ static parseObject(objStr) { objStr = objStr.trim(); // Remove trailing semicolon or period objStr = objStr.replace(/[.;]$/, '').trim(); // Literal with datatype const datatypeMatch = objStr.match(/^"([^"]+)"\^\^<(.+)>$/); if (datatypeMatch) { return { value: datatypeMatch[1], datatype: datatypeMatch[2] }; } // Literal with language const langMatch = objStr.match(/^"([^"]+)"@(.+)$/); if (langMatch) { return { value: langMatch[1], language: langMatch[2] }; } // Quoted string literal if ((objStr.startsWith('"') && objStr.endsWith('"')) || (objStr.startsWith("'") && objStr.endsWith("'"))) { return { value: objStr.slice(1, -1) }; } // IRI or blank node return this.expandIRI(objStr); } /** * Expand IRI (simplified - would need prefix resolution) */ static expandIRI(iri) { // Remove angle brackets if (iri.startsWith('<') && iri.endsWith('>')) { return iri.slice(1, -1); } // Handle prefixed names (simplified) if (iri.includes(':')) { const [prefix, local] = iri.split(':', 2); // In a full implementation, would resolve prefix return iri; } return iri; } /** * Group triples by subject */ static groupBySubject(triples) { const grouped = new Map(); for (const triple of triples) { if (!grouped.has(triple.subject)) { grouped.set(triple.subject, []); } grouped.get(triple.subject).push(triple); } return grouped; } } // Conditional fs import - only available in Node.js // Use dynamic import to avoid TypeScript errors in browser builds let fs = null; // @ts-ignore - Check for Node.js environment if (typeof window === 'undefined') { try { // @ts-ignore - fs is not available in browser, require may not exist const nodeRequire = typeof require !== 'undefined' ? require : null; if (nodeRequire) { fs = nodeRequire('fs'); } } catch { // fs not available fs = null; } } /** * SHACL Validator with enhanced Turtle/RDF parsing */ class ShaclValidator { /** * Load SHACL shapes from file */ async loadShapes(path) { if (!fs) { throw new Error('File system access not available in browser. Use loadShapesFromContent() instead.'); } const content = fs.readFileSync(path, 'utf-8'); // Try to parse as Turtle first try { return this.parseShapesFromTurtle(content); } catch (error) { // Fallback to simplified parser console.warn('Failed to parse as Turtle, using simplified parser:', error); return this.parseShapesSimple(content); } } /** * Parse shapes from Turtle content */ parseShapesFromTurtle(content) { const triples = TurtleParser.parse(content); const grouped = TurtleParser.groupBySubject(triples); const shapes = {}; // SHACL namespace const sh = 'http://www.w3.org/ns/shacl#'; const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; for (const [subject, subjectTriples] of grouped.entries()) { // Check if this is a NodeShape or PropertyShape const typeTriples = subjectTriples.filter(t => t.predicate === `${rdf}type` || t.predicate === 'rdf:type'); const isNodeShape = typeTriples.some(t => { const obj = typeof t.object === 'string' ? t.object : t.object.value; return obj === `${sh}NodeShape` || obj === 'sh:NodeShape'; }); const isPropertyShape = typeTriples.some(t => { const obj = typeof t.object === 'string' ? t.object : t.object.value; return obj === `${sh}PropertyShape` || obj === 'sh:PropertyShape'; }); if (isNodeShape || isPropertyShape) { const shape = this.buildShapeFromTriples(subject, subjectTriples, triples); shapes[subject] = shape; } } return shapes; } /** * Build shape from triples */ buildShapeFromTriples(subject, subjectTriples, allTriples) { const shape = { properties: [], constraints: []