feat: Implement scene rulebook and validation engine
- Added a new SceneRulebook system to manage data-driven validation rules for actions. - Introduced rule checks for actions like "take", "open", "move", "introduce", and "describe". - Created a rulebook engine to evaluate conditions and enforce rules during action validation. - Enhanced action handling with support for scene entry and character descriptions. - Updated the architecture documentation to reflect the new rule-based validation approach. - Added new endpoints and improved the persistence layer for rulebooks.
This commit is contained in:
94
charactergarden/app/src/contracts/rulebook.ts
Normal file
94
charactergarden/app/src/contracts/rulebook.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* SceneRulebook — data-driven validation rules for the truth engine.
|
||||
*
|
||||
* Rules are stored per-scene in the database and evaluated by rulebookEngine.ts.
|
||||
* The default set is seeded from defaultRulebook.ts and mirrors the original
|
||||
* hardcoded logic in truthEngine.ts.
|
||||
*/
|
||||
|
||||
/** Which entity in the action context a condition refers to. */
|
||||
export type EntityRole = "actor" | "target" | "actorRoom" | "targetRoom";
|
||||
|
||||
/**
|
||||
* A composable, JSON-serialisable condition expression.
|
||||
*
|
||||
* Combinators: and | or | not
|
||||
* Predicates:
|
||||
* entityExists — entity referenced by role is present in world state
|
||||
* entityExistsOrWillBeCreated — entity exists OR will be created earlier in this turn
|
||||
* entityType — entity.type === requiredType
|
||||
* eq / neq — entity field comparison (id, name, type, or attributes[attribute])
|
||||
* attributeExists — entity.attributes[attribute] is not undefined
|
||||
* sameLocation — two entities share the same location attribute value
|
||||
* actorIdIn — action.actorId is included in an allowed list
|
||||
* actorNameIn — actor.name matches one of an allowed list (case-insensitive)
|
||||
* attributeRef — entities[checkRole].attributes[prefix + entities[refRole].attributes[refAttribute]] === true
|
||||
* metaValueNotInRoom — no entity of entityType in actor's room has name === action.metadata[metaKey]
|
||||
*/
|
||||
export type ConditionExpr =
|
||||
| { op: "and"; conditions: ConditionExpr[] }
|
||||
| { op: "or"; conditions: ConditionExpr[] }
|
||||
| { op: "not"; condition: ConditionExpr }
|
||||
| { op: "entityExists"; role: EntityRole }
|
||||
| { op: "entityExistsOrWillBeCreated"; role: EntityRole }
|
||||
| { op: "entityType"; role: EntityRole; requiredType: string }
|
||||
| { op: "eq"; role: EntityRole; attribute: string; value: unknown }
|
||||
| { op: "neq"; role: EntityRole; attribute: string; value: unknown }
|
||||
| { op: "attributeExists"; role: EntityRole; attribute: string }
|
||||
| { op: "sameLocation"; roleA: EntityRole; roleB: EntityRole }
|
||||
| { op: "actorIdIn"; allowedIds: string[] }
|
||||
| { op: "actorNameIn"; allowedNames: string[] }
|
||||
| {
|
||||
op: "attributeRef";
|
||||
/** Entity whose attribute is being tested */
|
||||
checkRole: EntityRole;
|
||||
/** Optional string prepended to the resolved key (e.g. "has_") */
|
||||
prefix?: string;
|
||||
/** Entity that provides the dynamic attribute name */
|
||||
refRole: EntityRole;
|
||||
/** Attribute on refRole whose value supplies the key name */
|
||||
refAttribute: string;
|
||||
}
|
||||
| {
|
||||
op: "metaValueNotInRoom";
|
||||
/** Key in action.metadata whose value to match against entity names */
|
||||
metaKey: string;
|
||||
/** Only match entities of this type */
|
||||
entityType: string;
|
||||
};
|
||||
|
||||
/** A single named check within an action rule set. */
|
||||
export type RuleCheck = {
|
||||
id: string;
|
||||
/** Human-readable label shown in the rulebook editor. */
|
||||
description: string;
|
||||
condition: ConditionExpr;
|
||||
failReason: string;
|
||||
/**
|
||||
* Failure message template.
|
||||
* Supports: {actor.id}, {actor.name}, {target.id}, {target.name}
|
||||
*/
|
||||
failMessage: string;
|
||||
};
|
||||
|
||||
/** All checks that apply to a specific action type. */
|
||||
export type ActionRuleSet = {
|
||||
actionType: string;
|
||||
/**
|
||||
* When false, all checks are skipped and the action always passes.
|
||||
* Useful for quickly disabling enforcement without deleting the rules.
|
||||
*/
|
||||
enabled: boolean;
|
||||
checks: RuleCheck[];
|
||||
};
|
||||
|
||||
/** The full rulebook attached to a scene/world. */
|
||||
export type SceneRulebook = {
|
||||
id: string;
|
||||
worldId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
rules: ActionRuleSet[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
@@ -5,4 +5,6 @@ export type WorldState = {
|
||||
entities: Record<string, Entity>;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
/** ID of the SceneRulebook currently active for this world. */
|
||||
rulebookId?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user