Files
CharacterGardenStack/project.md
spencer ff9b86c3e9 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.
2026-04-26 13:33:05 -04:00

5.9 KiB

CharacterGarden MVP — Strict Architecture

🧠 Core Principle

The system is a deterministic simulation engine.
The LLM is not a source of truth. It is an input/output translator only.


🧩 System Architecture

User / LLM Input (Prose) ↓ [Parser Layer] ↓ [Normalization Layer] ↓ [Truth Engine (Validation)] ↓ [State Mutation Engine] ↓ [Persistence Layer] ↓ [LLM Output Generation]


🔒 Non-Negotiable Rules

  1. Truth Engine MUST NEVER parse natural language
  2. Only structured Actions may mutate world state
  3. All mutations must be validated before execution
  4. World state is modified ONLY through engine functions
  5. LLM output is never trusted without validation
  6. Every turn must be fully traceable

📦 Core Data Contracts

Action (STRICT UNION TYPE)

export type Action =
  | { type: "move"; actorId: string; targetId: string }
  | { type: "take"; actorId: string; targetId: string }
  | { type: "open"; actorId: string; targetId: string }
  | { type: "close"; actorId: string; targetId: string }
  | { type: "use"; actorId: string; targetId: string; toolId?: string }
  | { type: "speak"; actorId: string; content: string }
  | { type: "introduce"; actorId: string; targetId?: string; metadata?: Record<string, unknown> };

⚠️ DO NOT use generic type: string actions.


NormalizedAction

export type NormalizedAction = Action;

If invalid → reject BEFORE validation.


ValidationResult

export type ValidationResult = {
  success: boolean;
  reasonCode:
    | "OK"
    | "NOT_FOUND"
    | "NOT_PRESENT"
    | "LOCKED"
    | "INVALID_TARGET"
    | "MISSING_REQUIREMENT"
    | "OUT_OF_TURN"
    | "UNKNOWN";
  message: string;
};

Turn (Traceable Execution Unit)

export type Turn = {
  id: string;
  rawText: string;

  parsedActions: unknown[];
  normalizedActions: NormalizedAction[];

  validationResults: ValidationResult[];

  appliedActions: NormalizedAction[];

  timestamp: number;
};

🌍 World State (STRICT SCHEMA)

export type Entity = {
  id: string;
  name: string;
  locationId?: string;
  attributes?: Record<string, unknown>;
};

export type Location = {
  id: string;
  name: string;
  connectedTo: string[];
};

export type WorldState = {
  entities: Record<string, Entity>;
  locations: Record<string, Location>;
  inventory: Record<string, string[]>; // actorId → itemIds
  flags: Record<string, boolean>;
};

🧠 System Layers


1. Parser Layer (parser/)

Responsibility:

  • Convert prose → rough actions
  • May use LLM

Output:

unknown[]

⚠️ Parser output is NOT trusted.


2. Normalization Layer (parser/normalizeActions.ts)

Responsibility:

  • Enforce schema
  • Resolve references (hejohn)
  • Fill missing fields
  • Reject invalid structures

Input:

unknown[]

Output:

NormalizedAction[]

3. Truth Engine (engine/validate.ts)

Responsibility:

  • Determine if action is valid
  • MUST be deterministic
  • MUST NOT mutate state

Input:

NormalizedAction + WorldState

Output:

ValidationResult

4. Mutation Engine (engine/apply.ts)

Responsibility:

  • Apply ONLY successful actions
  • Mutate world state

Input:

NormalizedAction + WorldState

Output:

WorldState

5. Persistence Layer (storage/)

Responsibility:

  • Store:
    • world state
    • turns
    • history

🔄 Turn Execution Pipeline

function processTurn(rawText: string): Turn {
  const parsed = parse(rawText);

  const normalized = normalize(parsed);

  const validationResults = normalized.map(action =>
    validate(action, worldState)
  );

  const successfulActions = normalized.filter((_, i) =>
    validationResults[i].success
  );

  const newState = apply(successfulActions, worldState);

  const turn: Turn = {
    id: generateId(),
    rawText,
    parsedActions: parsed,
    normalizedActions: normalized,
    validationResults,
    appliedActions: successfulActions,
    timestamp: Date.now()
  };

  persist(turn, newState);

  return turn;
}

🧠 Reference Resolution Rules

Handled in normalization layer.

Examples:

Input Output
"he" actorId
"the door" door_1
"my key" key_owned_by_actor

Must be:

  • deterministic
  • context-aware
  • testable

🧪 Debug & Traceability (REQUIRED)

Every turn MUST store:

  • raw input
  • parsed output
  • normalized actions
  • validation results
  • applied actions

This enables:

  • replay
  • debugging
  • AI correction

⚙️ Initial Action Rules (MVP)

move

  • actor must exist
  • target must be connected location

take

  • item must be in same location
  • item not already owned

open

  • target must exist
  • if locked → fail unless key present

introduce

  • creates entity OR brings into scene

🚫 Anti-Patterns (DO NOT DO)

Let LLM mutate state
Store unvalidated actions
Use dynamic/untyped actions
Skip normalization
Combine validation + mutation
Allow hidden side effects


🚀 Future Extensions (Planned)

  • Memory system (vector + summaries)
  • Belief vs truth separation
  • Multi-agent turns
  • Time-based simulation
  • Rule plugins per scenario
  • UI action inspector

🧠 Mental Model

This system is:

A deterministic simulation engine with LLM-based I/O

NOT:

A chatbot with memory


Definition of Done (MVP)

  • Actions fully typed
  • Normalization layer implemented
  • Validation fully deterministic
  • State mutation isolated
  • Turn trace persisted
  • Simple scenario (door + key) works

💬 Guidance for Copilot

When generating code:

  • Prefer explicit types over generic objects
  • Avoid dynamic structures
  • Keep functions pure where possible
  • Do not introduce hidden state
  • Follow pipeline strictly