feat: implement core application structure with Fastify server and SQLite persistence

- Add Fastify server in `app/src/index.ts` with health check and API routes for game state and turn processing.
- Create `latentEntities.ts` to handle personal item plausibility and promote beliefs to facts based on actor context.
- Introduce `llmAdapter.ts` for action extraction from prose input.
- Develop `truthEngine.ts` for pure validation logic, handling all verbs with explicit rejection reasons.
- Define new types in `types.ts` for facts, affordances, and latent entity requests/resolutions.
- Update `docker-compose.yml` for improved service structure and volume management.
- Create frontend structure with React, including Dockerfile, Vite configuration, and initial components for state inspection.
- Implement basic styles and HTML structure for the frontend application.
- Document current status and next steps in `thoughts.md`.
This commit is contained in:
2026-04-23 21:08:38 -04:00
parent 14a07bca7a
commit 1df2ae8164
20 changed files with 1830 additions and 14 deletions

View File

@@ -0,0 +1,221 @@
import { FormEvent, useEffect, useState } from "react";
type Entity = {
id: string;
type: string;
name: string;
attributes: Record<string, unknown>;
};
type GameEvent = {
id: string;
turn: number;
result: "success" | "fail";
action: Record<string, unknown>;
timestamp: number;
};
type Turn = {
id: string;
turn: number;
input: string;
output: string;
timestamp: number;
};
type Belief = {
entity_id: string;
claim: string;
confidence: number;
};
type Summary = {
id: string;
turn_range: [number, number];
text: string;
timestamp: number;
};
type Snapshot = {
entities: Entity[];
events: GameEvent[];
turns: Turn[];
beliefs: Belief[];
summaries: Summary[];
};
type TurnResult = {
narration: string;
parser: string;
actions: Array<Record<string, unknown>>;
accepted: Array<Record<string, unknown>>;
rejected: Array<{ action: Record<string, unknown>; reason: string }>;
latent_resolution?: { accepted: boolean; reason: string; entity_id?: string };
snapshot: Snapshot;
};
const starterPrompts = [
"look around",
"open the gate",
"talk to the groundskeeper",
"go to the shed",
"pull out my phone",
];
async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json() as Promise<T>;
}
export default function App() {
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
const [latest, setLatest] = useState<TurnResult | null>(null);
const [input, setInput] = useState("look around");
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
void fetchJson<Snapshot>("/api/state")
.then((data) => {
setSnapshot(data);
setLoading(false);
})
.catch((fetchError: Error) => {
setError(fetchError.message);
setLoading(false);
});
}, []);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setSubmitting(true);
setError(null);
try {
const result = await fetchJson<TurnResult>("/api/turn", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input }),
});
setLatest(result);
setSnapshot(result.snapshot);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unknown error");
} finally {
setSubmitting(false);
}
}
return (
<main className="page-shell">
<section className="hero-panel">
<p className="eyebrow">CharacterGarden</p>
<h1>Bootable narrative sandbox</h1>
<p className="lede">
Submit a turn, inspect the current entities and events, and verify how the truth engine is mutating state.
</p>
<form className="turn-form" onSubmit={onSubmit}>
<label htmlFor="turn-input">Turn input</label>
<textarea
id="turn-input"
value={input}
onChange={(event) => setInput(event.target.value)}
rows={4}
placeholder="Type what the player does..."
/>
<div className="actions-row">
<button type="submit" disabled={submitting}>
{submitting ? "Submitting..." : "Run turn"}
</button>
<div className="chips">
{starterPrompts.map((prompt) => (
<button key={prompt} type="button" className="chip" onClick={() => setInput(prompt)}>
{prompt}
</button>
))}
</div>
</div>
</form>
{latest ? (
<section className="result-card">
<h2>Latest result</h2>
<p>{latest.narration}</p>
<pre>{JSON.stringify({ actions: latest.actions, rejected: latest.rejected, latent: latest.latent_resolution }, null, 2)}</pre>
</section>
) : null}
{error ? <p className="error-banner">{error}</p> : null}
</section>
<section className="inspector-grid">
<article className="panel">
<h2>World state</h2>
{loading && !snapshot ? <p>Loading...</p> : null}
<ul className="entity-list">
{snapshot?.entities.map((entity) => (
<li key={entity.id}>
<strong>{entity.name}</strong>
<span>{entity.type}</span>
<pre>{JSON.stringify(entity.attributes, null, 2)}</pre>
</li>
))}
</ul>
</article>
<article className="panel">
<h2>Turn log</h2>
<ul className="timeline-list">
{snapshot?.turns.slice().reverse().map((turn) => (
<li key={turn.id}>
<strong>Turn {turn.turn}</strong>
<p>{turn.input}</p>
<p>{turn.output}</p>
</li>
))}
</ul>
</article>
<article className="panel">
<h2>Events</h2>
<ul className="timeline-list compact">
{snapshot?.events.slice().reverse().map((event) => (
<li key={event.id}>
<strong>{event.result}</strong>
<pre>{JSON.stringify(event.action, null, 2)}</pre>
</li>
))}
</ul>
</article>
<article className="panel">
<h2>Beliefs & summaries</h2>
<h3>Beliefs</h3>
<ul className="timeline-list compact">
{snapshot?.beliefs.map((belief) => (
<li key={`${belief.entity_id}-${belief.claim}`}>
<strong>{belief.entity_id}</strong>
<p>{belief.claim}</p>
<p>confidence: {belief.confidence}</p>
</li>
))}
</ul>
<h3>Summaries</h3>
<ul className="timeline-list compact">
{snapshot?.summaries.map((summary) => (
<li key={summary.id}>
<strong>{summary.turn_range.join("-")}</strong>
<p>{summary.text}</p>
</li>
))}
</ul>
</article>
</section>
</main>
);
}