UNPKG

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