UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

364 lines (361 loc) 12 kB
// src/core/query/types.ts function isVariable(t) { return t.kind === "variable"; } function isLiteral(t) { return t.kind === "literal"; } function variable(name) { return { kind: "variable", name }; } function literal(value) { return { kind: "literal", value }; } // src/core/query/engine.ts var QueryEngine = class _QueryEngine { constructor(store) { this.store = store; } rules = /* @__PURE__ */ new Map(); maxRuleDepth = 32; /** Register a Datalog rule. Multiple rules with the same name = union. */ addRule(rule) { const existing = this.rules.get(rule.name) ?? []; existing.push(rule); this.rules.set(rule.name, existing); } removeRule(name) { this.rules.delete(name); } /** Execute a query against the store. */ execute(query) { const start = performance.now(); let results = this._evaluatePatterns(query.where, [/* @__PURE__ */ new Map()]); for (const filter of query.filters) { results = results.filter((b) => this._evalFilter(filter, b)); } if (query.aggregates.length > 0) { results = this._aggregate(results, query.aggregates, query.select); } if (query.orderBy.length > 0) { results = this._order(results, query.orderBy); } if (query.offset > 0) results = results.slice(query.offset); if (query.limit > 0) results = results.slice(0, query.limit); const projectVars = query.select.length > 0 ? [...query.select, ...query.aggregates.map((a) => a.as)] : []; const projected = this._project(results, projectVars); return { bindings: projected, executionTime: performance.now() - start, count: projected.length }; } // ------------------------------------------------------------------------- // Pattern evaluation // ------------------------------------------------------------------------- _evaluatePatterns(patterns, bindings) { let current = bindings; for (const pattern of patterns) { if (current.length === 0) break; current = this._evaluatePattern(pattern, current); } return current; } _evaluatePattern(pattern, bindings) { switch (pattern.kind) { case "fact": return this._evalFactPattern(pattern, bindings); case "link": return this._evalLinkPattern(pattern, bindings); case "not": return this._evalNotPattern(pattern, bindings); case "or": return this._evalOrPattern(pattern, bindings); case "rule": return this._evalRuleApplication(pattern, bindings); } } _evalFactPattern(p, bindings) { const results = []; for (const b of bindings) { const eResolved = this._resolve(p.entity, b); const aResolved = this._resolve(p.attribute, b); const vResolved = this._resolve(p.value, b); let facts; if (eResolved !== void 0 && aResolved !== void 0) { facts = this.store.getFactsByEntity(String(eResolved)).filter((f) => f.a === aResolved); } else if (eResolved !== void 0) { facts = this.store.getFactsByEntity(String(eResolved)); } else if (aResolved !== void 0 && vResolved !== void 0) { facts = this.store.getFactsByValue(String(aResolved), vResolved); } else if (aResolved !== void 0) { facts = this.store.getFactsByAttribute(String(aResolved)); } else { facts = this.store.getAllFacts(); } if (vResolved !== void 0) { facts = facts.filter((f) => f.v === vResolved); } for (const fact of facts) { const nb = new Map(b); if (this._bind(p.entity, fact.e, nb) && this._bind(p.attribute, fact.a, nb) && this._bind(p.value, fact.v, nb)) { results.push(nb); } } } return results; } _evalLinkPattern(p, bindings) { const results = []; for (const b of bindings) { const srcResolved = this._resolve(p.source, b); const attrResolved = this._resolve(p.attribute, b); const tgtResolved = this._resolve(p.target, b); let links; if (srcResolved !== void 0 && attrResolved !== void 0) { links = this.store.getLinksByEntityAndAttribute( String(srcResolved), String(attrResolved) ); } else if (srcResolved !== void 0) { links = this.store.getLinksByEntity(String(srcResolved)); } else if (attrResolved !== void 0) { links = this.store.getLinksByAttribute(String(attrResolved)); } else { links = this.store.getAllLinks(); } if (tgtResolved !== void 0) { links = links.filter((l) => l.e2 === tgtResolved); } for (const link of links) { const nb = new Map(b); if (this._bind(p.source, link.e1, nb) && this._bind(p.attribute, link.a, nb) && this._bind(p.target, link.e2, nb)) { results.push(nb); } } } return results; } _evalNotPattern(p, bindings) { return bindings.filter((b) => { const matches = this._evaluatePattern(p.pattern, [b]); return matches.length === 0; }); } _evalOrPattern(p, bindings) { const results = []; for (const branch of p.branches) { const branchResults = this._evaluatePatterns(branch, bindings); results.push(...branchResults); } return this._dedup(results); } _evalRuleApplication(p, bindings, depth = 0) { if (depth > this.maxRuleDepth) return []; const ruleDefs = this.rules.get(p.name); if (!ruleDefs) return []; const results = []; for (const b of bindings) { for (const rule of ruleDefs) { const ruleBindings = new Map(b); let ok = true; for (let i = 0; i < rule.params.length && i < p.args.length; i++) { const resolved = this._resolve(p.args[i], b); if (resolved !== void 0) { ruleBindings.set(rule.params[i], resolved); } else if (isVariable(p.args[i])) { } } if (!ok) continue; let bodyResults = this._evaluatePatterns(rule.body, [ruleBindings]); for (const f of rule.filters) { bodyResults = bodyResults.filter((rb) => this._evalFilter(f, rb)); } for (const rb of bodyResults) { const nb = new Map(b); for (let i = 0; i < rule.params.length && i < p.args.length; i++) { if (isVariable(p.args[i])) { const val = rb.get(rule.params[i]); if (val !== void 0) nb.set(p.args[i].name, val); } } results.push(nb); } } } return this._dedup(results); } // ------------------------------------------------------------------------- // Filtering // ------------------------------------------------------------------------- _evalFilter(filter, b) { const left = this._resolve(filter.left, b); const right = this._resolve(filter.right, b); if (left === void 0 || right === void 0) return false; switch (filter.op) { case "=": return left === right; case "!=": return left !== right; case "<": return left < right; case "<=": return left <= right; case ">": return left > right; case ">=": return left >= right; case "contains": return String(left).includes(String(right)); case "startsWith": return String(left).startsWith(String(right)); case "endsWith": return String(left).endsWith(String(right)); case "matches": return new RegExp(String(right)).test(String(left)); default: return false; } } // ------------------------------------------------------------------------- // Aggregation // ------------------------------------------------------------------------- _aggregate(bindings, aggregates, groupBy) { const aggVarNames = new Set(aggregates.map((a) => a.as)); const groupVars = groupBy.filter((v) => !aggVarNames.has(v)); const groups = /* @__PURE__ */ new Map(); for (const b of bindings) { const key = groupVars.map((v) => String(b.get(v) ?? "")).join("\0"); const group = groups.get(key) ?? []; group.push(b); groups.set(key, group); } const results = []; for (const [, group] of groups) { const nb = new Map(group[0]); for (const agg of aggregates) { const vals = group.map((b) => b.get(agg.variable)).filter((v) => v !== void 0); nb.set(agg.as, this._computeAggregate(agg.op, vals)); } results.push(nb); } return results; } _computeAggregate(op, vals) { switch (op) { case "count": return vals.length; case "sum": return vals.reduce( (s, v) => s + (Number(v) || 0), 0 ); case "avg": return vals.length ? vals.reduce( (s, v) => s + (Number(v) || 0), 0 ) / vals.length : 0; case "min": return vals.reduce( (m, v) => v < m ? v : m, vals[0] ); case "max": return vals.reduce( (m, v) => v > m ? v : m, vals[0] ); case "collect": return JSON.stringify(vals); default: return vals.length; } } // ------------------------------------------------------------------------- // Ordering // ------------------------------------------------------------------------- /** * Semantic rank for known enum values. EQL-S stores these as raw strings, so * a plain `<`/`>` comparison would be lexicographic (medium > critical). * Mapping values to ranks lets `ORDER BY ?priority` / `ORDER BY ?status` * honor the workflow order. Keyed by value (not attribute) since the tokens * are unambiguous across the two enums. */ static ENUM_RANKS = { critical: 0, high: 1, medium: 2, low: 3, backlog: 0, queue: 1, in_progress: 2, paused: 3, closed: 4 }; _order(bindings, orderBy) { return [...bindings].sort((a, b) => { for (const o of orderBy) { const va = a.get(o.variable); const vb = b.get(o.variable); if (va === vb) continue; if (va === void 0) return 1; if (vb === void 0) return -1; const sa = String(va); const sb = String(vb); const ra = _QueryEngine.ENUM_RANKS[sa]; const rb = _QueryEngine.ENUM_RANKS[sb]; const cmp = ra !== void 0 && rb !== void 0 ? ra < rb ? -1 : 1 : sa < sb ? -1 : 1; return o.direction === "asc" ? cmp : -cmp; } return 0; }); } // ------------------------------------------------------------------------- // Projection // ------------------------------------------------------------------------- _project(bindings, select) { return bindings.map((b) => { const row = {}; if (select.length === 0) { for (const [k, v] of b) row[k] = v; } else { for (const s of select) { const v = b.get(s); if (v !== void 0) row[s] = v; } } return row; }); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- _resolve(term, bindings) { if (isLiteral(term)) return term.value; return bindings.get(term.name); } _bind(term, value, bindings) { if (isLiteral(term)) return term.value === value; const existing = bindings.get(term.name); if (existing !== void 0) return existing === value; bindings.set(term.name, value); return true; } _dedup(bindings) { const seen = /* @__PURE__ */ new Set(); return bindings.filter((b) => { const key = [...b.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([k, v]) => `${k}=${v}`).join("\0"); if (seen.has(key)) return false; seen.add(key); return true; }); } }; export { isVariable, isLiteral, variable, literal, QueryEngine };