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

364 lines
5.9 KiB
Markdown

# 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)
```ts
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
```ts
export type NormalizedAction = Action;
```
If invalid → reject BEFORE validation.
---
## ValidationResult
```ts
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)
```ts
export type Turn = {
id: string;
rawText: string;
parsedActions: unknown[];
normalizedActions: NormalizedAction[];
validationResults: ValidationResult[];
appliedActions: NormalizedAction[];
timestamp: number;
};
```
---
# 🌍 World State (STRICT SCHEMA)
```ts
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:**
```ts
unknown[]
```
⚠️ Parser output is NOT trusted.
---
## 2. Normalization Layer (`parser/normalizeActions.ts`)
**Responsibility:**
- Enforce schema
- Resolve references (`he``john`)
- Fill missing fields
- Reject invalid structures
**Input:**
```ts
unknown[]
```
**Output:**
```ts
NormalizedAction[]
```
---
## 3. Truth Engine (`engine/validate.ts`)
**Responsibility:**
- Determine if action is valid
- MUST be deterministic
- MUST NOT mutate state
**Input:**
```ts
NormalizedAction + WorldState
```
**Output:**
```ts
ValidationResult
```
---
## 4. Mutation Engine (`engine/apply.ts`)
**Responsibility:**
- Apply ONLY successful actions
- Mutate world state
**Input:**
```ts
NormalizedAction + WorldState
```
**Output:**
```ts
WorldState
```
---
## 5. Persistence Layer (`storage/`)
**Responsibility:**
- Store:
- world state
- turns
- history
---
# 🔄 Turn Execution Pipeline
```ts
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