trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
661 lines (657 loc) • 23.3 kB
JavaScript
// src/core/ontology/registry.ts
var OntologyRegistry = class {
schemas = /* @__PURE__ */ new Map();
/** Resolved entity defs (with inherited attributes merged). */
resolvedEntities = /* @__PURE__ */ new Map();
/** Resolved relation defs. */
resolvedRelations = /* @__PURE__ */ new Map();
/**
* Register an ontology schema.
* Throws if an ontology with the same ID is already registered at the same version.
*/
register(schema) {
const existing = this.schemas.get(schema.id);
if (existing && existing.version === schema.version) {
throw new Error(
`Ontology "${schema.id}" v${schema.version} is already registered.`
);
}
this.schemas.set(schema.id, schema);
this._resolve(schema);
}
/**
* Unregister an ontology by ID.
*/
unregister(id) {
const schema = this.schemas.get(id);
if (!schema) return;
for (const [name, entry] of this.resolvedEntities) {
if (entry.ontologyId === id) {
this.resolvedEntities.delete(name);
}
}
for (const [name, entries] of this.resolvedRelations) {
const filtered = entries.filter((e) => e.ontologyId !== id);
if (filtered.length === 0) {
this.resolvedRelations.delete(name);
} else {
this.resolvedRelations.set(name, filtered);
}
}
this.schemas.delete(id);
}
/**
* Get a registered ontology schema by ID.
*/
get(id) {
return this.schemas.get(id);
}
/**
* List all registered ontology schemas.
*/
list() {
return [...this.schemas.values()];
}
/**
* Get the resolved entity definition for an entity type name.
* Returns the entity def with inherited attributes merged in.
*/
getEntityDef(typeName) {
return this.resolvedEntities.get(typeName)?.def;
}
/**
* Get the ontology ID that defines a given entity type.
*/
getEntityOntology(typeName) {
return this.resolvedEntities.get(typeName)?.ontologyId;
}
/**
* List all known entity type names.
*/
listEntityTypes() {
return [...this.resolvedEntities.keys()];
}
/**
* Get all relation definitions involving a given entity type
* (either as source or target).
*/
getRelationsForType(typeName) {
const results = [];
for (const [, entries] of this.resolvedRelations) {
for (const entry of entries) {
if (entry.def.sourceTypes.includes(typeName) || entry.def.targetTypes.includes(typeName)) {
results.push(entry.def);
}
}
}
return results;
}
/**
* Get a specific relation definition by name.
*/
getRelationDef(name) {
const entries = this.resolvedRelations.get(name);
return entries?.[0]?.def;
}
/**
* List all known relation names.
*/
listRelationNames() {
return [...this.resolvedRelations.keys()];
}
/**
* Check if an entity type is known to any registered ontology.
*/
hasEntityType(typeName) {
return this.resolvedEntities.has(typeName);
}
/**
* Get the required attributes for an entity type.
*/
getRequiredAttributes(typeName) {
const def = this.getEntityDef(typeName);
if (!def) return [];
return def.attributes.filter((a) => a.required);
}
// -------------------------------------------------------------------------
// Resolution (inheritance)
// -------------------------------------------------------------------------
_resolve(schema) {
for (const entity of schema.entities) {
const resolved = this._resolveEntity(entity, schema);
this.resolvedEntities.set(entity.name, {
def: resolved,
ontologyId: schema.id
});
}
for (const relation of schema.relations) {
const existing = this.resolvedRelations.get(relation.name) ?? [];
existing.push({ def: relation, ontologyId: schema.id });
this.resolvedRelations.set(relation.name, existing);
}
}
_resolveEntity(entity, schema) {
if (!entity.extends) return entity;
let parent = schema.entities.find((e) => e.name === entity.extends);
if (!parent) {
const resolved = this.resolvedEntities.get(entity.extends);
parent = resolved?.def;
}
if (!parent) {
throw new Error(
`Entity "${entity.name}" extends "${entity.extends}" which is not defined.`
);
}
const resolvedParent = this._resolveEntity(parent, schema);
const childAttrNames = new Set(entity.attributes.map((a) => a.name));
const mergedAttrs = [
...resolvedParent.attributes.filter((a) => !childAttrNames.has(a.name)),
...entity.attributes
];
return {
...entity,
attributes: mergedAttrs
};
}
};
// src/core/ontology/builtins.ts
var projectOntology = {
id: "trellis:project",
name: "Project Ontology",
version: "1.0.0",
description: "Entity types for software project management.",
entities: [
{
name: "Project",
description: "A software project or repository.",
attributes: [
{ name: "name", type: "string", required: true, description: "Project name" },
{ name: "description", type: "string", description: "Project description" },
{ name: "status", type: "string", enum: ["active", "archived", "draft", "deprecated"], default: "active" },
{ name: "url", type: "string", description: "Project URL or repository link" },
{ name: "language", type: "string", description: "Primary programming language" },
{ name: "createdAt", type: "date", description: "Creation timestamp" },
{ name: "updatedAt", type: "date", description: "Last update timestamp" }
]
},
{
name: "Module",
description: "A logical module or package within a project.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "path", type: "string", description: "Filesystem path relative to project root" },
{ name: "description", type: "string" }
]
},
{
name: "Feature",
description: "A product feature or capability.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "status", type: "string", enum: ["planned", "in-progress", "shipped", "cut"], default: "planned" },
{ name: "priority", type: "string", enum: ["critical", "high", "medium", "low"], default: "medium" }
]
},
{
name: "Dependency",
description: "An external dependency or library.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "version", type: "string" },
{ name: "registry", type: "string", description: "Package registry (npm, pypi, etc.)" },
{ name: "scope", type: "string", enum: ["runtime", "dev", "optional"], default: "runtime" }
]
},
{
name: "Config",
description: "A configuration entry or setting.",
attributes: [
{ name: "key", type: "string", required: true },
{ name: "value", type: "any", required: true },
{ name: "description", type: "string" },
{ name: "scope", type: "string", enum: ["project", "user", "system"], default: "project" }
]
},
{
name: "Artifact",
description: "A build artifact, release asset, or output file.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "path", type: "string" },
{ name: "size", type: "number" },
{ name: "hash", type: "string" },
{ name: "format", type: "string" }
]
},
{
name: "Release",
description: "A versioned release of a project.",
attributes: [
{ name: "version", type: "string", required: true },
{ name: "tag", type: "string" },
{ name: "date", type: "date" },
{ name: "notes", type: "string" },
{ name: "status", type: "string", enum: ["draft", "published", "yanked"], default: "draft" }
]
}
],
relations: [
{ name: "contains", sourceTypes: ["Project"], targetTypes: ["Module", "Feature", "Config"], cardinality: "many", description: "Project contains modules/features/configs" },
{ name: "dependsOn", sourceTypes: ["Project", "Module"], targetTypes: ["Dependency", "Module", "Project"], cardinality: "many", description: "Depends on another entity" },
{ name: "implementedBy", sourceTypes: ["Feature"], targetTypes: ["Module"], cardinality: "many", description: "Feature is implemented by modules" },
{ name: "produces", sourceTypes: ["Project", "Release"], targetTypes: ["Artifact"], cardinality: "many", description: "Produces artifacts" },
{ name: "releases", sourceTypes: ["Project"], targetTypes: ["Release"], cardinality: "many", description: "Project has releases" }
]
};
var teamOntology = {
id: "trellis:team",
name: "Team Ontology",
version: "1.0.0",
description: "Entity types for team and developer organization.",
entities: [
{
name: "Team",
description: "A team or organizational group.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "slug", type: "string", description: "URL-safe identifier" }
]
},
{
name: "Developer",
description: "A developer or contributor.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "email", type: "string" },
{ name: "handle", type: "string", description: "Username or handle" },
{ name: "role", type: "string", enum: ["admin", "maintainer", "contributor", "reviewer"] }
]
},
{
name: "Role",
description: "A named role with specific permissions.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "permissions", type: "string", unique: false, description: "Permission strings (multi-valued)" }
]
},
{
name: "Capability",
description: "A skill or capability.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "category", type: "string" },
{ name: "level", type: "string", enum: ["beginner", "intermediate", "advanced", "expert"] }
]
}
],
relations: [
{ name: "hasMember", sourceTypes: ["Team"], targetTypes: ["Developer"], cardinality: "many", inverse: "memberOf", description: "Team has member" },
{ name: "memberOf", sourceTypes: ["Developer"], targetTypes: ["Team"], cardinality: "many", inverse: "hasMember", description: "Developer is member of team" },
{ name: "owns", sourceTypes: ["Developer"], targetTypes: ["Project", "Module"], cardinality: "many", description: "Developer owns/maintains" },
{ name: "reviewsFor", sourceTypes: ["Developer"], targetTypes: ["Project", "Module"], cardinality: "many", description: "Developer reviews for" },
{ name: "hasCapability", sourceTypes: ["Developer"], targetTypes: ["Capability"], cardinality: "many", description: "Developer has capability" },
{ name: "hasRole", sourceTypes: ["Developer"], targetTypes: ["Role"], cardinality: "many", description: "Developer has role" },
{ name: "assignedTo", sourceTypes: ["Developer"], targetTypes: ["Feature"], cardinality: "many", description: "Developer is assigned to feature" }
]
};
var agentOntology = {
id: "trellis:agent",
name: "Agent Ontology",
version: "1.0.0",
description: "Entity types for AI agents, runs, plans, and tools.",
entities: [
{
name: "Agent",
description: "An AI agent definition.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "model", type: "string", description: "LLM model identifier" },
{ name: "provider", type: "string", description: "LLM provider (openai, anthropic, local, etc.)" },
{ name: "systemPrompt", type: "string" },
{ name: "status", type: "string", enum: ["active", "inactive", "deprecated"], default: "active" }
]
},
{
name: "AgentCapability",
description: "A capability or skill an agent possesses.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "category", type: "string" }
]
},
{
name: "AgentRun",
description: "A single execution run of an agent.",
attributes: [
{ name: "startedAt", type: "date", required: true },
{ name: "completedAt", type: "date" },
{ name: "status", type: "string", enum: ["running", "plan_pending", "paused", "completed", "failed", "cancelled"], default: "running" },
{ name: "input", type: "string" },
{ name: "output", type: "string" },
{ name: "totalTokens", type: "number" },
{ name: "promptTokens", type: "number" },
{ name: "completionTokens", type: "number" }
]
},
{
name: "AgentPlan",
description: "A plan or strategy created by an agent.",
attributes: [
{ name: "title", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "status", type: "string", enum: ["draft", "active", "completed", "abandoned"], default: "draft" }
]
},
{
name: "Tool",
description: "A tool available to agents.",
attributes: [
{ name: "name", type: "string", required: true },
{ name: "description", type: "string" },
{ name: "schema", type: "string", description: "JSON schema for tool parameters" },
{ name: "endpoint", type: "string" }
]
}
],
relations: [
{ name: "hasCapability", sourceTypes: ["Agent"], targetTypes: ["AgentCapability"], cardinality: "many" },
{ name: "hasTool", sourceTypes: ["Agent"], targetTypes: ["Tool"], cardinality: "many" },
{ name: "executedBy", sourceTypes: ["AgentRun"], targetTypes: ["Agent"], cardinality: "one" },
{ name: "hasPlan", sourceTypes: ["AgentRun"], targetTypes: ["AgentPlan"], cardinality: "many" },
{ name: "usedTool", sourceTypes: ["AgentRun"], targetTypes: ["Tool"], cardinality: "many" },
{ name: "createdBy", sourceTypes: ["AgentPlan"], targetTypes: ["Agent"], cardinality: "one" }
]
};
var builtinOntologies = [
projectOntology,
teamOntology,
agentOntology
];
// src/core/ontology/validator.ts
function validateEntity(entityId, store, registry) {
const errors = [];
const warnings = [];
const facts = store.getFactsByEntity(entityId);
if (facts.length === 0) {
return { valid: true, errors: [], warnings: [] };
}
const typeFact = facts.find((f) => f.a === "type");
if (!typeFact) {
warnings.push({
entityId,
entityType: "(unknown)",
field: "type",
message: 'Entity has no "type" attribute.',
severity: "warning"
});
return { valid: true, errors, warnings };
}
const entityType = String(typeFact.v);
const def = registry.getEntityDef(entityType);
if (!def) {
warnings.push({
entityId,
entityType,
field: "type",
message: `Entity type "${entityType}" is not defined in any registered ontology.`,
severity: "warning"
});
return { valid: true, errors, warnings };
}
if (def.abstract) {
errors.push({
entityId,
entityType,
field: "type",
message: `Cannot instantiate abstract entity type "${entityType}".`,
severity: "error"
});
}
for (const attr of def.attributes) {
if (attr.required && attr.name !== "type") {
const hasFact = facts.some((f) => f.a === attr.name);
if (!hasFact) {
errors.push({
entityId,
entityType,
field: attr.name,
message: `Required attribute "${attr.name}" is missing.`,
severity: "error"
});
}
}
}
for (const fact of facts) {
if (fact.a === "type" || fact.a === "createdAt" || fact.a === "updatedAt") continue;
const attrDef = def.attributes.find((a) => a.name === fact.a);
if (!attrDef) {
warnings.push({
entityId,
entityType,
field: fact.a,
message: `Attribute "${fact.a}" is not defined in the "${entityType}" ontology.`,
severity: "warning"
});
continue;
}
const typeErr = validateAttrType(fact.v, attrDef);
if (typeErr) {
errors.push({
entityId,
entityType,
field: fact.a,
message: typeErr,
severity: "error"
});
}
if (attrDef.enum && !attrDef.enum.includes(fact.v)) {
errors.push({
entityId,
entityType,
field: fact.a,
message: `Value "${fact.v}" is not in allowed values: [${attrDef.enum.join(", ")}].`,
severity: "error"
});
}
if (attrDef.pattern && typeof fact.v === "string") {
if (!new RegExp(attrDef.pattern).test(fact.v)) {
errors.push({
entityId,
entityType,
field: fact.a,
message: `Value "${fact.v}" does not match pattern /${attrDef.pattern}/.`,
severity: "error"
});
}
}
if (attrDef.min !== void 0) {
if (typeof fact.v === "number" && fact.v < attrDef.min) {
errors.push({
entityId,
entityType,
field: fact.a,
message: `Value ${fact.v} is below minimum ${attrDef.min}.`,
severity: "error"
});
}
if (typeof fact.v === "string" && fact.v.length < attrDef.min) {
errors.push({
entityId,
entityType,
field: fact.a,
message: `String length ${fact.v.length} is below minimum ${attrDef.min}.`,
severity: "error"
});
}
}
if (attrDef.max !== void 0) {
if (typeof fact.v === "number" && fact.v > attrDef.max) {
errors.push({
entityId,
entityType,
field: fact.a,
message: `Value ${fact.v} exceeds maximum ${attrDef.max}.`,
severity: "error"
});
}
if (typeof fact.v === "string" && fact.v.length > attrDef.max) {
errors.push({
entityId,
entityType,
field: fact.a,
message: `String length ${fact.v.length} exceeds maximum ${attrDef.max}.`,
severity: "error"
});
}
}
}
const links = store.getLinksByEntity(entityId);
for (const link of links) {
if (link.e1 !== entityId) continue;
const relDef = registry.getRelationDef(link.a);
if (!relDef) continue;
if (!relDef.sourceTypes.includes(entityType)) {
errors.push({
entityId,
entityType,
field: link.a,
message: `Entity type "${entityType}" is not allowed as source for relation "${link.a}".`,
severity: "error"
});
}
const targetFacts = store.getFactsByEntity(link.e2);
const targetType = targetFacts.find((f) => f.a === "type");
if (targetType && !relDef.targetTypes.includes(String(targetType.v))) {
errors.push({
entityId,
entityType,
field: link.a,
message: `Target type "${targetType.v}" is not allowed for relation "${link.a}" (expected: ${relDef.targetTypes.join(", ")}).`,
severity: "error"
});
}
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
function validateStore(store, registry) {
const allErrors = [];
const allWarnings = [];
const typeFacts = store.getFactsByAttribute("type");
const entityIds = new Set(typeFacts.map((f) => f.e));
for (const entityId of entityIds) {
const result = validateEntity(entityId, store, registry);
allErrors.push(...result.errors);
allWarnings.push(...result.warnings);
}
return {
valid: allErrors.length === 0,
errors: allErrors,
warnings: allWarnings
};
}
function validateAttrType(value, def) {
if (def.type === "any") return null;
switch (def.type) {
case "string":
if (typeof value !== "string") return `Expected string, got ${typeof value}.`;
break;
case "number":
if (typeof value !== "number") return `Expected number, got ${typeof value}.`;
break;
case "boolean":
if (typeof value !== "boolean") return `Expected boolean, got ${typeof value}.`;
break;
case "date":
if (typeof value === "string") {
if (isNaN(Date.parse(value))) return `Expected ISO date string, got "${value}".`;
} else if (!(value instanceof Date)) {
return `Expected date, got ${typeof value}.`;
}
break;
case "ref":
if (typeof value !== "string") return `Expected entity reference (string), got ${typeof value}.`;
break;
}
return null;
}
function createValidationMiddleware(registry, options) {
const strict = options?.strict ?? false;
return {
name: "ontology-validator",
handleOp: (op, ctx, next) => {
if (op.facts && op.facts.length > 0) {
for (const fact of op.facts) {
if (fact.a === "type") continue;
if (fact.a === "createdAt" || fact.a === "updatedAt") continue;
const typeFact = op.facts.find((f) => f.e === fact.e && f.a === "type");
if (!typeFact) continue;
const entityType = String(typeFact.v);
const def = registry.getEntityDef(entityType);
if (!def) {
if (strict) {
throw new Error(
`[ontology-validator] Unknown entity type "${entityType}" for entity "${fact.e}".`
);
}
continue;
}
const attrDef = def.attributes.find((a) => a.name === fact.a);
if (!attrDef) {
if (strict) {
throw new Error(
`[ontology-validator] Unknown attribute "${fact.a}" for type "${entityType}".`
);
}
continue;
}
const typeErr = validateAttrType(fact.v, attrDef);
if (typeErr) {
throw new Error(
`[ontology-validator] Entity "${fact.e}" attribute "${fact.a}": ${typeErr}`
);
}
if (attrDef.enum && !attrDef.enum.includes(fact.v)) {
throw new Error(
`[ontology-validator] Entity "${fact.e}" attribute "${fact.a}": value "${fact.v}" not in [${attrDef.enum.join(", ")}].`
);
}
}
}
if (op.links && op.links.length > 0) {
for (const link of op.links) {
const relDef = registry.getRelationDef(link.a);
if (!relDef) continue;
const sourceTypeFact = op.facts?.find(
(f) => f.e === link.e1 && f.a === "type"
);
if (sourceTypeFact && !relDef.sourceTypes.includes(String(sourceTypeFact.v))) {
throw new Error(
`[ontology-validator] Relation "${link.a}": source type "${sourceTypeFact.v}" not allowed (expected: ${relDef.sourceTypes.join(", ")}).`
);
}
}
}
return next(op, ctx);
}
};
}
export {
OntologyRegistry,
projectOntology,
teamOntology,
agentOntology,
builtinOntologies,
validateEntity,
validateStore,
createValidationMiddleware
};