trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
548 lines (542 loc) • 17.1 kB
JavaScript
// src/core/computation/expr-evaluator.ts
var ExprEvaluator = class {
functions;
constructor() {
this.functions = /* @__PURE__ */ new Map([
["$if", this.if.bind(this)],
["$round", this.round.bind(this)],
["$concat", this.concat.bind(this)],
["$len", this.len.bind(this)],
["$lower", this.lower.bind(this)],
["$upper", this.upper.bind(this)],
["$trim", this.trim.bind(this)],
["$now", this.now.bind(this)],
["$uuid", this.uuid.bind(this)],
["$add", this.add.bind(this)],
["$sub", this.sub.bind(this)],
["$mul", this.mul.bind(this)],
["$div", this.div.bind(this)],
["$mod", this.mod.bind(this)],
["$eq", this.eq.bind(this)],
["$ne", this.ne.bind(this)],
["$gt", this.gt.bind(this)],
["$gte", this.gte.bind(this)],
["$lt", this.lt.bind(this)],
["$lte", this.lte.bind(this)],
["$and", this.and.bind(this)],
["$or", this.or.bind(this)],
["$not", this.not.bind(this)],
["$coalesce", this.coalesce.bind(this)],
["$contains", this.contains.bind(this)],
["$startsWith", this.startsWith.bind(this)],
["$endsWith", this.endsWith.bind(this)]
]);
}
/**
* Evaluate an expression string against a context.
*/
eval(expr, context) {
const trimmed = expr.trim();
const fnMatch = trimmed.match(/^(\$\w+)\((.*)\)$/s);
if (fnMatch) {
const fnName = fnMatch[1];
const argsStr = fnMatch[2];
const fn = this.functions.get(fnName);
if (!fn) {
throw new Error(`Unknown function: ${fnName}`);
}
return this.evalFunction(fn, argsStr, context);
}
if (trimmed.startsWith("$")) {
const field = trimmed.slice(1);
return context[field];
}
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if (trimmed === "null") return null;
const num = Number(trimmed);
if (!isNaN(num) && trimmed !== "") return num;
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
return trimmed.slice(1, -1);
}
return trimmed;
}
evalFunction(fn, argsStr, context) {
const args = this.parseArgs(argsStr, context);
return fn(...args);
}
parseArgs(argsStr, context) {
const args = [];
let depth = 0;
let current = "";
for (let i = 0; i < argsStr.length; i++) {
const char = argsStr[i];
if (char === "(" || char === "[" || char === "{") depth++;
else if (char === ")" || char === "]" || char === "}") depth--;
else if (char === "," && depth === 0) {
args.push(this.eval(current.trim(), context));
current = "";
continue;
}
current += char;
}
if (current.trim()) {
args.push(this.eval(current.trim(), context));
}
return args;
}
// Built-in functions
if(cond, trueVal, falseVal) {
return cond ? trueVal : falseVal;
}
round(value, decimals = 0) {
const n = Number(value);
const factor = Math.pow(10, Number(decimals));
return Math.round(n * factor) / factor;
}
concat(...args) {
return args.map((a) => String(a ?? "")).join("");
}
len(value) {
if (typeof value === "string") return value.length;
if (Array.isArray(value)) return value.length;
return 0;
}
lower(value) {
return String(value ?? "").toLowerCase();
}
upper(value) {
return String(value ?? "").toUpperCase();
}
trim(value) {
return String(value ?? "").trim();
}
now() {
return (/* @__PURE__ */ new Date()).toISOString();
}
uuid() {
return crypto.randomUUID();
}
add(...args) {
return args.reduce((sum, a) => sum + Number(a), 0);
}
sub(a, b) {
return Number(a) - Number(b);
}
mul(...args) {
return args.reduce((prod, a) => prod * Number(a), 1);
}
div(a, b) {
return Number(a) / Number(b);
}
mod(a, b) {
return Number(a) % Number(b);
}
eq(a, b) {
return a === b;
}
ne(a, b) {
return a !== b;
}
gt(a, b) {
return Number(a) > Number(b);
}
gte(a, b) {
return Number(a) >= Number(b);
}
lt(a, b) {
return Number(a) < Number(b);
}
lte(a, b) {
return Number(a) <= Number(b);
}
and(...args) {
return args.every(Boolean);
}
or(...args) {
return args.some(Boolean);
}
not(value) {
return !value;
}
coalesce(...args) {
for (const arg of args) {
if (arg !== null && arg !== void 0) return arg;
}
return null;
}
contains(haystack, needle) {
return String(haystack ?? "").includes(String(needle ?? ""));
}
startsWith(str, prefix) {
return String(str ?? "").startsWith(String(prefix ?? ""));
}
endsWith(str, suffix) {
return String(str ?? "").endsWith(String(suffix ?? ""));
}
};
function evalExpr(expr, context) {
const result = new ExprEvaluator().eval(expr, context);
if (result === null || result === void 0) return "";
if (typeof result === "boolean") return result ? "true" : "false";
if (typeof result === "number") return result;
if (typeof result === "string") return result;
if (typeof result === "object") return JSON.stringify(result);
return String(result);
}
// src/core/computation/rollup.ts
function collectRollupRelatedIds(rollup, ctx) {
if (rollup.joinEntity) {
const { type, foreignKey } = rollup.joinEntity;
const facts = ctx.store.getFactsByAttribute(foreignKey);
const ids = [];
for (const fact of facts) {
if (fact.v !== ctx.entityId) continue;
const typeFacts = ctx.store.getFactsByEntity(fact.e);
const entityType = typeFacts.find((f) => f.a === "type")?.v;
if (entityType === type) ids.push(fact.e);
}
return ids;
}
const linkAttr = resolveLinkAttribute(rollup.relationProperty, ctx.schema);
const links = ctx.store.getLinksByEntityAndAttribute(ctx.entityId, linkAttr);
if (links.length > 0) return links.map((l) => l.e2);
return [];
}
function resolveLinkAttribute(relationProperty, schema) {
const field = schema?.fields.find((f) => f.name === relationProperty);
if (field?.valueType === "relation") return relationProperty;
return relationProperty;
}
function readTargetValue(store, entityId, targetProperty) {
if (targetProperty === "id") return entityId;
const facts = store.getFactsByEntity(entityId);
return facts.find((f) => f.a === targetProperty)?.v;
}
function toNumbers(values) {
const nums = [];
for (const v of values) {
if (typeof v === "number" && Number.isFinite(v)) nums.push(v);
}
return nums;
}
function evaluateRollup(rollup, ctx) {
const related = collectRollupRelatedIds(rollup, ctx);
if (rollup.aggregation === "count") {
return related.length;
}
const values = related.map((id) => readTargetValue(ctx.store, id, rollup.targetProperty)).filter((v) => v !== void 0);
const nums = toNumbers(values);
if (nums.length === 0) return 0;
switch (rollup.aggregation) {
case "sum":
return nums.reduce((a, b) => a + b, 0);
case "avg":
return nums.reduce((a, b) => a + b, 0) / nums.length;
case "min":
return Math.min(...nums);
case "max":
return Math.max(...nums);
case "median": {
const sorted = [...nums].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}
case "mode": {
const freq = /* @__PURE__ */ new Map();
for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1);
let best = nums[0];
let bestCount = 0;
for (const [n, c] of freq) {
if (c > bestCount) {
best = n;
bestCount = c;
}
}
return best;
}
default:
return 0;
}
}
function projectRelationFields(binding, schema, store, entityId) {
for (const field of schema.fields) {
if (field.valueType !== "relation" || field.name in binding) continue;
const links = store.getLinksByEntityAndAttribute(entityId, field.name);
if (links.length === 0) continue;
const cardinality = field.relation?.cardinality ?? "many";
if (cardinality === "one") {
binding[field.name] = links[0].e2;
} else {
binding[field.name] = links.map((l) => l.e2).join(",");
}
}
}
// src/core/kernel/logic-middleware.ts
function createLogicMiddleware(config) {
return {
name: "logic-computation",
handleQuery(query, ctx, next) {
const result = next(query);
if (result && Array.isArray(result.bindings)) {
const store = resolveStore(config, ctx);
for (const binding of result.bindings) {
enrichBinding(binding, config, store);
}
}
return result;
}
};
}
function resolveStore(config, ctx) {
if (config.getStore) return config.getStore();
return ctx.store;
}
function enrichBinding(binding, config, store) {
const row = binding;
const type = resolveEntityType(binding, config);
if (!type) return;
const schema = config.ontologies.get(type);
if (!schema) return;
const entityId = findEntityId(binding);
if (store && entityId) {
projectRelationFields(binding, schema, store, entityId);
}
for (const field of schema.fields) {
if (!(field.computed || field.formula || field.rollup || field.aiGenerated))
continue;
const fieldName = field.name;
if (fieldName in binding) continue;
if (field.formula) {
binding[fieldName] = evalExpr(field.formula, row);
}
if (field.rollup && store && entityId) {
binding[fieldName] = evaluateRollup(field.rollup, {
store,
entityId,
schema,
getEntityType: config.getEntityType
});
}
if (field.aiGenerated && config.generateAiField) {
const prompt = field.aiGenerated.prompt.replace(
/\{\{(\w+)\}\}/g,
(_, key) => String(row[key] ?? "")
);
config.generateAiField(prompt, row).then((value) => {
binding[fieldName] = value;
}).catch(() => {
binding[fieldName] = "";
});
}
}
}
function resolveEntityType(binding, config) {
const explicit = binding.type;
if (typeof explicit === "string" && explicit.length > 0) return explicit;
const entityId = findEntityId(binding);
if (entityId && config.getEntityType) {
return config.getEntityType(entityId);
}
return void 0;
}
function findEntityId(binding) {
for (const value of Object.values(binding)) {
if (typeof value === "string" && value.includes(":")) return value;
}
return void 0;
}
// src/core/kernel/schema-middleware.ts
function createSchemaMiddleware(config) {
const strict = config.strict ?? true;
return {
name: "schema-validation",
async handleOp(op, ctx, next) {
if (!op.facts || op.facts.length === 0) {
return next(op, ctx);
}
const ontologies = config.getOntologies();
const errors = [];
const entities = new Set(
op.facts.filter((fact) => fact.a === "type").map((fact) => fact.e)
);
for (const entityId of entities) {
const typeFact = op.facts.find((fact) => fact.e === entityId && fact.a === "type");
if (!typeFact) continue;
const entityType = String(typeFact.v);
const schema = resolveSchemaForEntity(entityType, entityId, op.facts, ontologies);
if (!schema) continue;
for (const fact of op.facts) {
if (fact.e !== entityId) continue;
const fieldSpec = schema.fields.find((field) => field.name === fact.a);
if (!fieldSpec) continue;
const validationError = validateValue(fact.a, fact.v, fieldSpec);
if (validationError) errors.push(validationError);
}
for (const fieldSpec of schema.fields) {
if (!fieldSpec.required) continue;
const hasValue = op.facts.some(
(fact) => fact.e === entityId && fact.a === fieldSpec.name && fact.v !== null && fact.v !== void 0 && fact.v !== ""
);
if (!hasValue) {
errors.push(`Missing required field: ${fieldSpec.name} on entity ${entityId}`);
}
}
}
if (errors.length > 0 && strict) {
const errorMsg = [...new Set(errors)].join("; ");
throw new Error(`Schema validation failed: ${errorMsg}`);
}
return next(op, ctx);
}
};
}
function collectionSlugFromCollectionId(collectionId) {
const prefix = "collectionMeta:";
if (!collectionId.startsWith(prefix)) return null;
const slug = collectionId.slice(prefix.length).trim();
return slug || null;
}
function findPerCollectionRecordSchema(slug, ontologies) {
const suffix = `/collections/${slug}/Record`;
for (const schema of ontologies.values()) {
if (schema["@id"].endsWith(suffix)) return schema;
}
return void 0;
}
function resolveSchemaForEntity(entityType, entityId, facts, ontologies) {
const shortType = entityType.includes(":") ? entityType.split(":").pop() : entityType;
if (shortType === "CollectionRecord") {
const collectionIdFact = facts.find(
(fact) => fact.e === entityId && fact.a === "collectionId"
);
const slug = collectionIdFact ? collectionSlugFromCollectionId(String(collectionIdFact.v)) : null;
if (slug) {
const perCollection = findPerCollectionRecordSchema(slug, ontologies);
if (perCollection?.fields?.length) return perCollection;
}
}
return ontologies.get(entityType) ?? ontologies.get(shortType) ?? ontologies.get(shortType.toLowerCase()) ?? ontologies.get(entityType.toLowerCase());
}
function validateValue(fieldName, value, spec) {
if (value === null || value === void 0) {
return null;
}
const actualType = typeof value;
switch (spec.valueType) {
case "title":
case "rich_text":
case "select":
case "multi_select":
case "status":
case "phone_number":
case "url":
case "email": {
if (actualType !== "string") {
return `Field ${fieldName}: expected string, got ${actualType}`;
}
const text = value;
if (spec.minLength !== void 0 && text.length < spec.minLength) {
return `Field ${fieldName}: must be at least ${spec.minLength} characters`;
}
if (spec.maxLength !== void 0 && text.length > spec.maxLength) {
return `Field ${fieldName}: must be at most ${spec.maxLength} characters`;
}
if (spec.pattern) {
try {
if (!new RegExp(spec.pattern).test(text)) {
return `Field ${fieldName}: invalid format`;
}
} catch {
}
}
if (spec.selectOptions && spec.selectOptions.length > 0) {
if (!spec.selectOptions.includes(value)) {
return `Field ${fieldName}: value "${value}" not in allowed options`;
}
}
break;
}
case "number":
if (actualType !== "number") {
return `Field ${fieldName}: expected number, got ${actualType}`;
}
if (spec.min !== void 0 && value < spec.min) {
return `Field ${fieldName}: value ${value} below minimum ${spec.min}`;
}
if (spec.max !== void 0 && value > spec.max) {
return `Field ${fieldName}: value ${value} above maximum ${spec.max}`;
}
break;
case "checkbox":
if (actualType !== "boolean") {
return `Field ${fieldName}: expected boolean, got ${actualType}`;
}
break;
case "date":
if (actualType === "string") {
const date = new Date(value);
if (isNaN(date.getTime())) {
return `Field ${fieldName}: invalid date format`;
}
} else if (actualType !== "object") {
return `Field ${fieldName}: expected date string or object, got ${actualType}`;
}
break;
case "files":
case "people":
case "relation":
case "rollup":
case "formula":
case "ai_generated":
case "json":
break;
default:
break;
}
return null;
}
// src/core/kernel/boot-middleware.ts
function buildOntologyIndex(schemas) {
const map = /* @__PURE__ */ new Map();
for (const schema of schemas) {
map.set(schema["@id"], schema);
const short = schema["@id"].includes(":") ? schema["@id"].split(":").pop() : schema["@id"];
if (short) {
map.set(short, schema);
map.set(short.toLowerCase(), schema);
}
if (schema.label) {
map.set(schema.label, schema);
map.set(schema.label.toLowerCase(), schema);
}
}
return map;
}
function attachStandardMiddleware(kernel) {
kernel.removeMiddleware("logic-computation");
kernel.removeMiddleware("schema-validation");
const getOntologies = () => buildOntologyIndex(kernel.listOntologies());
kernel.addMiddleware(
createSchemaMiddleware({
getOntologies
})
);
kernel.addMiddleware(
createLogicMiddleware({
ontologies: getOntologies(),
getStore: () => kernel.getStore(),
getEntityType: (entityId) => kernel.getEntity(entityId)?.type
})
);
}
export {
ExprEvaluator,
evalExpr,
collectRollupRelatedIds,
evaluateRollup,
projectRelationFields,
createLogicMiddleware,
buildOntologyIndex,
attachStandardMiddleware
};