Cynario Cynario alpha
Browse docs

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

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.

FileWhat it carries
types.tsThe AST shape, the allow-listed operators, the depth / node-count caps, the reserved-key rules — the sandbox contract
evaluate.tsThe pure interpreter (evaluate, safeEvaluate, applyActions, validateAst). No iteration ops, so it always terminates
implicitState.tsDerives 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 / onExit actions 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/

ComponentRole
ScenarioBuilder.svelteThe visual graph editor — the main authoring surface, built on Svelte Flow
ScenarioPlayer.svelteThe 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 — the docs collection schema (title, group, order, navLabel). The group enum 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 by GROUP_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 evaluatessrc/lib/logic/evaluate.ts (+ types.ts for the contract)
How a played node behavessrc/lib/runtime/playerRuntime.ts
Dynamic text in contentsrc/lib/templating/render.ts
Accepting a scenario safelysrc/lib/validateScenario.ts
Rendering untrusted HTMLsrc/lib/sanitize.ts
The graph editorsrc/components/ScenarioBuilder.svelte
The play experiencesrc/components/ScenarioPlayer.svelte
Undo / redo / statesrc/stores/ (scenarioStore, historyStore)
These docssrc/content/docs/ + src/content/config.ts + DocsLayout.astro