Zorg Knowledge

Zorg's Dungeon Maker

Knowledge Base & Traceability Graph

Architecture Decision Records (ADR)

ADR-0001: Record architecture decisions as ADRs

  • Status: Accepted
  • Date: 2026-09-09
  • Context

    GAME_SPEC.md captures what the game is (rules, FR/NFR). It doesn't

    capture why we built it a particular way, and that reasoning otherwise

    lives only in chat history or a contributor's head — which the AEA framework

    explicitly excludes as shared memory ("Chat is not shared memory";

    architecture.artof.link/comparison.html).

    Decision

    Every non-trivial technical decision (stack choice, engine/UI split,

    data format, a reversal of an earlier decision) gets a short ADR in

    docs/adr/, numbered sequentially, following TEMPLATE.md. ADRs are never

    renumbered or deleted (same ID-freeze discipline as FR/NFR IDs).

    Consequences

    Anyone — human or AI agent — picking this repo up cold reads AGENTS.md,

    then GAME_SPEC.md, then skims docs/adr/ to understand not just the rules

    but the reasoning already spent, instead of re-litigating settled questions.

    ---

    ADR-0002: TypeScript monorepo, 2D now, 3D-and-native deferred behind one renderer interface

  • Status: Accepted
  • Date: 2026-09-09
  • Context

    Target progression is 2D (board-game style) now, 3D later, deployed to

    laptop (web), then tablets and phones (Android first for validation).

    NFR-1 (determinism) and NFR-4/NFR-10 (fast, isolated, headless testing of

    the solver and hero AI) need to hold throughout that progression.

    A game engine such as Unity or Godot would also reach web+mobile+3D from one

    project, and a Unity license is available if this direction is revisited.

    The trade-off: those engines fuse simulation and presentation, which makes

    the deterministic, UI-free unit testing NFR-1/NFR-10 ask for harder to keep

    cheap in CI (headless render setups, license activation per CI run), and

    makes scripts/check-requirements-trace.mjs (linking FR/NFR IDs to plain

    test files) awkward against engine-native test frameworks.

    Decision

  • packages/engine: pure TypeScript, no DOM/engine dependency. Owns every
  • FR-xx/NFR-xx behavior and is exhaustively unit-tested (Vitest) in CI in

    seconds, with no build/device step.

  • apps/web: the Maker, Vite + React. Renders the dungeon on a plain 2D
  • canvas now (matches the board-game phase). A future 3D pass swaps in

    three.js / react-three-fiber behind the same room-state props — engine

    code does not change.

  • Laptop target: apps/web running directly in a browser.
  • Tablet/phone target (Android first): wrap the built apps/web output with
  • Capacitor to produce an installable Android app with zero UI fork. Revisit

    React Native only if Capacitor's WebView performance becomes a real

    constraint once 3D lands.

  • Figma is available for mocking up the Maker's UI before building it, and
  • the existing Unity license stays a fallback if the 2D-canvas → three.js →

    Capacitor path hits a wall — not adopted now, per the trade-off above.

    Consequences

    One engine codebase, one UI codebase, for the whole 2D→3D / laptop→tablet→

    phone progression. The only two things that change over time are which

    renderer is mounted (canvas vs. three.js) and which shell runs apps/web

    (browser vs. Capacitor) — packages/engine never has to know.

    ---

    ADR-0003: Multi-agent collaboration across OpenAI, Claude, Grok, AGY, Copilot, Kimi, and DeepSeek

  • Status: Accepted
  • Date: 2026-09-10
  • Context

    Development on this repository involves multiple autonomous AI agents and coding assistants across distinct providers and frameworks — including Anthropic Claude, OpenAI/Codex, xAI Grok, Google Antigravity / Gemini (AGY), GitHub Copilot, Moonshot Kimi, and DeepSeek.

    Without strict architectural constraints:

    1. Each tool introduces proprietary configuration formats or instructions that drift out of sync.

    2. Agents assume ephemeral chat history represents shared knowledge, leading to forgotten context between different agent sessions.

    3. Concurrent agent sessions can rebase or overwrite each other's in-flight work or make simultaneous uncoordinated edits to single-source ledgers (docs/STATUS_LEDGER.md, docs/FINDINGS.md).

    4. Cross-platform checkout differences (e.g., Windows CRLF vs. Linux/Mac LF in CI containers) cause diff noise across agents.

    Decision

    1. Single Source of Truth (AGENTS.md): All agent guidelines reside exclusively in AGENTS.md. Tool-specific entrypoints (CLAUDE.md, GEMINI.md, CODEX.md, OPENAI.md, GROK.md, KIMI.md, DEEPSEEK.md, .github/copilot-instructions.md, .cursorrules) are strict, minimal pointers that redirect directly to AGENTS.md. No tool-specific instructions may diverge.

    2. Shared Memory Discipline: Chat history is not shared memory. The default branch, PR branches, and committed docs (GAME_SPEC.md, docs/STATUS_LEDGER.md, docs/FINDINGS.md, docs/journal/) are the sole shared memory between agents.

    3. Isolated Workspaces & Branch Convention: Agents work on separate topic branches named agent/<agent-family>/<task-slug> or copilot/<task-slug>. Agents must never rebase over another in-flight branch.

    4. Peer Review & Verification: A producer agent does not self-approve or merge its own changes. Pull requests must pass automated CI checks (pnpm test) and governance checks (pnpm governance) before review and merge by another agent or human.

    5. Cross-Platform Line Ending Normalization: Enforce eol=lf via .gitattributes so agents operating across heterogeneous environments (Windows, Linux, macOS) produce uniform diffs.

    Consequences

  • Any supported agent immediately discovers AGENTS.md and operates under identical constraints without prompt drift.
  • Multi-agent collaboration is asynchronous, safe from race conditions, and fully auditable through git history and daily dev journal entries.
  • Automated CI and governance gates guarantee that every agent adheres to requirements-trace and honesty validation regardless of model architecture.
  • ---

    ADR-0004: Deployment architecture, knowledge site, and observability

  • Status: Accepted
  • Date: 2026-09-10
  • Context

    Following the dual-site architecture of the Adaptive Experience Architecture framework (architecture.artof.link + aea.artof.link) and Café Fausse (knowledge.cafe.artof.link + cafe.artof.link), Zorg's Dungeon Maker requires two distinct public endpoints:

    1. Implementation & Playback view (zorg.artof.link): Interactive 2D Maker grid canvas and extermination simulation runner.

    2. Knowledge Base (knowledge.zorg.artof.link): Canonical reference for game rules, requirements traceability graph, honesty ledger, coherence findings, and dev journals.

    3. Observability (https://zorg.artof.link/grafana/): Public monitoring of host and container metrics, matching https://aea.artof.link/grafana/.

    Evaluation between AWS ECS Fargate + ALB vs. AWS Lightsail + Docker Compose revealed:

  • ECS Fargate + ALB costs \$45–\$75+/month baseline (ALB alone is \$18–\$22/mo) and requires complex multi-task coordination and EFS volume CSI drivers for Prometheus TSDB storage.
  • AWS Lightsail (small_3_0, \$12/mo flat) provides 2 vCPUs, 2 GB RAM, 60 GB SSD, 3 TB egress, and static IP. It hosts a self-contained Docker Compose stack with zero cloud sprawl and matches the existing operational model of cafe.artof.link.
  • Decision

    1. Implementation Compute: Deploy zorg.artof.link on AWS Lightsail instance zorg-dungeon-prod (Ubuntu 24.04, small_3_0) in us-east-1a with static IP <LIGHTSAIL_STATIC_IP>.

    2. Implementation Stack: Use Docker Compose running:

    - web: Multi-stage build of @zorg/engine and @zorg/web served by Nginx.

    - caddy: Reverse proxy providing automated Let's Encrypt TLS and reverse proxying /grafana/ to Grafana and / to the web app.

    - prometheus: Scrapes node and web metrics with local TSDB persistence.

    - grafana: Served at /grafana/ with anonymous viewer access enabled, pre-provisioned Prometheus datasource, and system dashboards.

    - node-exporter: Exposes host CPU, RAM, disk, and network metrics.

    3. Knowledge Site: Host knowledge.zorg.artof.link on GitHub Pages (artofdream.github.io.) built via GitHub Actions (.github/workflows/knowledge.yml) from markdown files in docs/ and GAME_SPEC.md.

    4. DNS Management: Manage records in Route 53 zone <HOSTED_ZONE_ID> (artof.link.):

    - zorg.artof.link. $\rightarrow$ A <LIGHTSAIL_STATIC_IP> (TTL: 60)

    - knowledge.zorg.artof.link. $\rightarrow$ CNAME artofdream.github.io. (TTL: 300)

    Consequences

  • Hosting costs are minimized to \$12/mo flat with zero load balancer idle fees.
  • The knowledge site is decoupled from compute and globally distributed via GitHub CDN at zero cost.
  • Full observability is publicly visible under /grafana/ without credentials required for inspection.
  • Amendment (2026-09-28, after the default-admin exposure fixed in #67): the public /grafana/ route is anonymous read-only. Caddy returns 404 for the login form, admin UI and admin APIs, and 403 for any credentialed request (basic auth, bearer token, Grafana session cookie) or any write other than the dashboards' POST /api/ds/query. Grafana basic auth is disabled. Admin access is only over an SSH tunnel to Grafana's host-local port 127.0.0.1:3000 (procedure in CONTRIBUTING.md, "Grafana admin access"). deploy-web verifies the tunnel-path admin login and probes the public surface on every deploy.
  • Amendment (2026-09-28, logging): Caddy writes a JSON access log for the whole site (incl. /grafana) to the caddy_logs volume (roll at 20 MiB, keep ≤14 files, ≤14 days); Grafana logs to console and to a file in the grafana_logs volume (daily rotation, 14 days) with request logging on. The logs contain client IPs, so retention is deliberately short (14 days); both survive container recreation. Details in CONTRIBUTING.md, "Production logs".
  • Amendment (2026-09-28, secrets and Caddy admin): Grafana's secret_key comes from the GRAFANA_SECRET_KEY repo secret and is delivered like the admin password (SSH stdin → root-only /etc/zorg/grafana.env); a key change is refused while Grafana stores encrypted secrets and is followed by a data-key rotation. The Caddy admin API is bound to localhost inside its container; Prometheus scrapes Caddy metrics on an internal :9180 listener. Repo secrets are interim until the sponsor's 3DX Lab per-project secrets method is adopted.
  • Grafana's admin password is never committed: it lives in the GRAFANA_ADMIN_PASSWORD repo secret, is written by deploy-web to a root-only host env file (/etc/zorg/grafana.env), and is re-applied to Grafana's DB on every deploy (the env var alone only applies when the DB is first created).
  • The production stack can be run verbatim on local development machines using Docker Desktop.
  • ---

    ADR-0005: Authored campaign first, then a Difficulté-banded generator

  • Status: Accepted
  • Date: 2026-09-12
  • Context

    Sponsor chose authored campaign first, then generator. Phases 0–7 plus the

    Difficulté campaign browser already ship the fixture pack. A generator that

    invented new room types, Gunner duration, contract gating, or a fantasy

    difficulty scale would violate NFR-8 and the deferred rulings (S2, S3, S4,

    S6). Chat is not shared memory — the knobs and guarantees need to live next

    to the code.

    Decision

  • generateLevel({ difficulty, seed?, caps? }) lives in @zorg/engine
  • (packages/engine/src/generator.ts). Pure library: LevelDef +

    serializeLevel text + a suggested Maker layout. No UI dependency.

  • Difficulté targets are the authored numeric bands 1–4 (the values that
  • actually appear on green fixtures). Knobs (room / hero / spell counts, E/O

    flavour) stay inside those corpus envelopes (band 4 matches Stupidity

    Award's 9-room board). Unspecified deluxe contracts are not a generation

    band.

  • Typical generated worlds are intended solvable under FR-46 / NFR-4:
  • an A → lethal D → … → Z line, first D ≥ hero HP, empty cast sequence.

    This slice does not emit mirror worlds or not_solvable-mirror constraints.

  • Out of the generator (still legal elsewhere): C (FR-18 / S2), Gunner
  • (avoids S3 duration), Mechanic / Princess (pathing can break the corridor),

    P / T (portals and unpaid tolls), choix, contracts 11–15, FR-4 gating.

  • Web: authored campaign stays the default; Generate / Regenerate is additive.
  • Honesty copy: generated = Simulated engine, not Live & Probed.

    Consequences

  • Players can roll a practice dungeon after the campaign without a new FR.
  • Proof is packages/engine/src/generator.test.ts — Simulated, not a production
  • probe. No STATUS_LEDGER row is promoted by this decision.

  • See PLAYER_GUIDE, 2026-09-12, STATUS_LEDGER.
  • ---

    ADR-0006: Grounded constraint evaluation, refuse opaque prose

  • Status: Accepted
  • Date: 2026-09-12
  • Context

    FR-43 and FR-44 sit on top of the Phase 6 runner: worlds already

    resolve normal → M′ → M″ (FR-45), and FR-46 can ask whether a world

    is solvable. The loader already stores Constraints / Bonuses / Variante as

    prose expression strings. GAME_SPEC names the extra win-condition

    categories — positional dist equations, death order, HP floors,

    gold-at-death, solvability of mirror worlds — but it does not define a

    constraint language. Authored fixtures mix those categories with layout

    shape, counterfactuals, visit-set comparisons, and setup variants.

    Inventing evaluators for the opaque lines would violate NFR-8 (never

    silently guess) the same way inventing room C or Gunner duration would.

    Decision

  • scoreLevel in packages/engine/src/constraints.ts is the FR-43 /
  • FR-44 layer. It calls resolveWorlds (FR-45 order), then aggregates:

    any world Z-entry is a loss; a win requires every hero in every world

    dead and every blocking constraint held.

  • Bonuses (and source Variante lines) are evaluated the same way but never
  • block the aggregate outcome.

  • Only authored patterns that map onto GAME_SPEC categories and an
  • existing engine sensor are classified:

    - solvability of named worlds → checkSolvability (budget ⇒ unsupported,

    not a proof)

    - dist equations / ∃-adjacent / ∃-D-pair → FR-10 Manhattan

    - HP-floor / gold-at-death / reverse death-order prose → simulation events

  • Every other line is UnsupportedConstraint. An unsupported blocking
  • constraint refuses a win claim (unresolved). The engine does not guess

    “square dungeon”, spell-usage, distinct death rooms, immunity

    counterfactuals, or C / Gunner duration / FR-4.

  • Per-world simulate still reports the scheduler outcome only. Maker
  • playback is unchanged.

    Consequences

  • FR-43 / FR-44 can be Simulated for the grounded subset. Opaque
  • corpus lines stay unevaluated — that is a documented gap, not a silent

    pass.

  • Adding a new evaluator requires a GAME_SPEC hook or an existing sensor,
  • plus a test. See STATUS_LEDGER, 2026-09-12, PLAYER_GUIDE.

    ---

    ADR-0007: Sustainable FR/EN i18n (display-only)

  • Status: Accepted
  • Date: 2026-09-13
  • Context

    The Maker and knowledge companions ship English-first copy for kids (~8) and

    helpers. A French audience needs the same surfaces without rewriting engine

    tokens, inventing unavailable rules, or forking GAME_SPEC. Prior notes said

    “no i18n rewrite” for hatch copy (NFR-7); that was scope deferral, not a

    ban on a catalog-based display layer.

    Constraints that still bind:

  • GAME_SPEC English IDs and engine tokens (A / Z / D / …, hero type
  • names in data) stay the legal voice. Companions never override the spec.

  • Do not invent room C, Gunner shot-duration, or FR-4 contract
  • gating (NFR-8 fail-closed). Honesty disclosure may name those gaps;

    playable copy must not claim they are built.

  • Phase C (bilingual honesty ledger / skills matrix / full GAME_SPEC) is out
  • of scope for this decision.

    Decision

    1. Message catalogs under apps/web/src/locales/{en,fr}.ts hold every

    user-facing Maker string. EN keys are the source of truth; FR is a complete

    parallel catalog. Display-only: translate words players see (Start/Départ,

    Exit/Sortie, Difficulty/Difficulté), keep engine tokens and FR/NFR IDs as

    data.

    2. Tiny t(key) + locale context in apps/web/src/i18n/. Default locale

    is EN. No React Router in this slice — URL strategy is ?lang=fr

    (canonical), with document.documentElement.lang, localStorage key

    zorg-locale, and a chrome switcher (EN | FR) on landing/Maker. Path

    prefix /fr/ is reserved for a later router slice; do not invent Vite

    rewrites here.

    3. CI missing-key gate: every EN key exists in FR and vice versa (vitest).

    4. Knowledge FR companions live under docs/fr/ (LEARN.md,

    PLAYER_GUIDE.md, PLAYER_JOURNEYS.md). The knowledge build emits

    fr/learn.html, fr/guide.html, fr/journeys.html with EN↔FR nav.

    French prose cites the same FR-xx / NFR-xx IDs; if French and the

    spec disagree, the spec wins. Honesty / skills / full GAME_SPEC stay

    English until Phase C.

    5. Maker “Learn the rules” points at the FR learn page when locale is FR.

    Consequences

  • Easier: bilingual Maker chrome and kid/helper copy without engine forks.
  • Harder: catalogs must stay complete; new UI strings need EN+FR in the same
  • PR or CI fails.

  • Out of scope: Phase C honesty/skills bilingual; inventing C / Gunner
  • duration / FR-4; path-prefix routing without a router.

  • See PLAYER_JOURNEYS (optional FR probe), LEARN, 2026-09-13.
  • ---