Agent context
Codebase map — where things live, what's load-bearing
An orientation to the modules that carry the weight and the seams between them — the logic AST engine, the player runtime, the templating layer, the import gate, the two big Svelte surfaces, and the docs collection. Where to look first, not an exhaustive file listing.
On this page
- The big picture
- The logic AST engine — src/lib/logic/
- The player runtime — src/lib/runtime/playerRuntime.ts
- The templating layer — src/lib/templating/render.ts
- The import gate + storage — src/lib/validateScenario.ts, src/lib/storage.ts
- The two Svelte surfaces — src/components/
- The docs collection — src/content/docs/ (this site)
- Where to look first
This is a map, not an inventory. It points at the modules that carry the weight and the seams between them — the boundaries where untrusted data is checked, where state changes, where the UI hands off to a pure core. Learn these and the rest of the tree reads itself.
The repository root is written here as <repo-root>; all paths are relative to
it.
The big picture
scenario JSON ──► IMPORT GATE ──► STORE ──► RUNTIME ──► TEMPLATING ──► SANITIZE ──► screen
(file / P2P / validate- scenario player- render.ts sanitize.ts
pasted) Scenario.ts Store.ts Runtime.ts (read-only) (the chokepoint)
│ │
fail closed every condition/expression
on bad AST runs through the SANDBOXED
logic evaluator (no eval)
Two Svelte surfaces sit on top: the Builder (graph editor) writes the scenario, the Player drives the runtime to play it. Both render the same data model.
The logic AST engine — src/lib/logic/
The heart of the safety story. Scenario logic is stored as a JSON-native AST
(JsonLogic-shaped: a node is a JSON value, an operator is a single-key object),
and run by our own interpreter — never eval, never Function.
| File | What it carries |
|---|---|
types.ts | The AST shape, the allow-listed operators, the depth / node-count caps, the reserved-key rules — the sandbox contract |
evaluate.ts | The pure interpreter (evaluate, safeEvaluate, applyActions, validateAst). No iteration ops, so it always terminates |
implicitState.ts | Derives implicit state (_visits, _visited, _path, _choices) from the play history |
Why it’s load-bearing: every condition and expression in a played scenario flows through here, and scenarios arrive untrusted over P2P. The interpreter is allow-listed, depth/size-capped, and proto-pollution-safe by construction — this is the WHEN/IF/DO logic from the concept docs, made real.
The player runtime — src/lib/runtime/playerRuntime.ts
The per-node “tick”: a pure, framework-agnostic function that turns the data model into a played experience. The Svelte player just drives it, so the same core could power a preview or a test harness.
The one-way flow per node:
enter node → onEnter actions mutate STATE → build read-only CONTEXT
(state + implicit + computed) → gate outputs + template content (read-only)
→ choose an output → onChoice + onExit mutate STATE → enter target
Two rules make this trustworthy, and both echo the truth model:
- Reads never write. Context-building, gating, and templating are read-only;
only
onEnter/onChoice/onExitactions mutate state. - Fail closed, never crash. Every read routes through
safeEvaluate, so a bad expression yields a fallback instead of throwing mid-play.
captureSink.ts alongside it records the one input/RNG/time value per step into
the run log, so a replay re-reads the same value instead of re-prompting — the
mechanism behind deterministic replay and share.
The templating layer — src/lib/templating/render.ts
A Liquid-subset for content: {{ score }}, {{ name | upper }},
{% if passed %}…{% endif %}. Safe by design because content is untrusted on
receipt:
- tokens resolve against the curated, proto-safe evaluation context — never live JS objects;
- filters are a closed allow-list (authors can’t define new ones);
- interpolated values are HTML-escaped, so a value can’t inject markup;
- the final output still passes through
renderMarkdownSafe.
This is the read-only Bindings layer from the mental model: Bindings read, Logic writes.
The import gate + storage — src/lib/validateScenario.ts, src/lib/storage.ts
Scenarios arrive untrusted (file, P2P, pasted JSON). validateScenario.ts is the
single import gate: it caps structural size and walks every embedded logic
AST through the allow-list/limits validator, hard-failing anything the
evaluator couldn’t safely run. Nothing reaches the runtime un-vetted.
storage.ts is the localStorage persistence layer (load / save / export-import)
and the home of schema migration (v1 choices[] → v2 outputs[] on load).
It routes imports through validateScenario, so the gate is the choke point even
for locally-loaded data.
src/lib/sanitize.ts is the other chokepoint: the single place untrusted
markdown/HTML becomes safe HTML (marked → DOMPurify allow-list). Anything we
inject with {@html} has been through here.
The four seams to remember: import gate (
validateScenario), sandboxed evaluator (logic/evaluate), sanitize chokepoint (sanitize), and the pure runtime (playerRuntime). Untrusted data crosses each one and is checked there — not scattered through the UI.
The two Svelte surfaces — src/components/
| Component | Role |
|---|---|
ScenarioBuilder.svelte | The visual graph editor — the main authoring surface, built on Svelte Flow |
ScenarioPlayer.svelte | The narrative player — drives playerRuntime and renders each node’s settled result |
Supporting them: custom Svelte Flow nodes/edges under components/nodes/ and
components/edges/, the editors (NodeEditor, ActionsEditor,
ContentEditorModal), and the Slides mode (SlidesView, src/lib/slides/).
State lives in src/stores/ — scenarioStore (the document), historyStore
(the undo/redo operation log), plus settings, connection, theme,
confirm. Mutations go through store functions so undo/redo stays intact; direct
store mutation is an anti-pattern.
The docs collection — src/content/docs/ (this site)
The pages you’re reading are a native Astro content collection, not a separate docs tool:
src/content/config.ts— thedocscollection schema (title,group,order,navLabel). Thegroupenum is the set of sidebar sections.src/content/docs/**.md— one Markdown file per page; the file path becomes the route (agent-context/codebase-map.md→/docs/agent-context/codebase-map).src/pages/docs/[...slug].astro— renders each entry through the layout.src/layouts/DocsLayout.astro— the on-brand shell: builds the sidebar tree from the collection (ordered byGROUP_ORDER), renders the prose with the design tokens, and handles the mobile nav / TOC.
To add a docs page: drop a Markdown file in the right group folder with frontmatter
group + order. No Starlight, no extra config — it renders through
SiteLayout and the design tokens like the rest
of the site.
Where to look first
| If you’re touching… | Start at… |
|---|---|
| How a rule evaluates | src/lib/logic/evaluate.ts (+ types.ts for the contract) |
| How a played node behaves | src/lib/runtime/playerRuntime.ts |
| Dynamic text in content | src/lib/templating/render.ts |
| Accepting a scenario safely | src/lib/validateScenario.ts |
| Rendering untrusted HTML | src/lib/sanitize.ts |
| The graph editor | src/components/ScenarioBuilder.svelte |
| The play experience | src/components/ScenarioPlayer.svelte |
| Undo / redo / state | src/stores/ (scenarioStore, historyStore) |
| These docs | src/content/docs/ + src/content/config.ts + DocsLayout.astro |