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
JavaScript
// 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
};