# Architecture Six primitives. Every feature is a composition of them — never a special case, never a plugin slot bolted to the side. This file is the target the code follows. STATUS says what is **legal to build now** — a permission, not a report. What actually exists is `state.md`'s job alone, so a STATUS line never needs editing as code lands. ``` request ──► resolver ──► Bundle ──► [Stage…] ──► View ──► bytes │ ▲ │ Query ─────────┘ cache Interaction ──► (own path, off the content graph) change / clock / command ──► Effect ──► derived artifacts, outbound calls ``` ## Bundle — the atom A content folder (or single file): metadata + one or more language bodies + local assets + attached interactions. Identity is the bundle; language is a variant of it. **STATUS: buildable.** Arc 1's spine: single-language, single-file bundles first, then language fallback. ## Stage — a transform during rendering `func(ctx, *Page) error`, in fixed order. Every rendering feature is a Stage: typography, Bengali numerals, shortcodes, image sizing, dithering. A Stage runs on everything by default and is switched off by a settings cascade key (`ideas/deferred-decisions.md`), never by a type predicate compiled into it; a Stage with nothing to do returns early on content shape. **STATUS: deferred.** Called inline in the render path until the transform counter in `state.md` reaches its threshold. Check it; do not guess. ## Query — filter/sort bundles into a list Returns a list plus a signature doubling as a cache key. All grouping is a Query: sections, tags, series, latest, related, pagination, feeds. **STATUS: live.** Earned at the first collection page. `content.Query{Section, Lang}` with `Site.Run`; it carries no cache signature yet, because nothing caches and a signature with no consumer is speculation. ## View — bundle-or-query → output Overridable per bundle. All presentation. Emits HTML, gemtext, PDF, a program, JSON. **STATUS: deferred until Arc 2.** Until then, templates chosen by a small explicit lookup. Its data contract is frozen from the moment a theme exists, not at Arc 2 — see `theme-contract.md` (ADR-0023). ## Interaction — external input attached to a bundle Comments, webmentions, likes, via adapters. Off the content dependency graph: own fragment cache, one-page blast radius, hydrated client-side or spliced late. **STATUS: not buildable yet.** Arc 3. Nothing may assume it exists. ## Effect — work outside the request path Derived artifacts and outbound calls: image derivatives, search index, sitemap and feed files, webmention sending, POSSE, outbound-link archiving, EPUB builds, future-dated publication. Three triggers, and one Effect may accept more than one: **on change** (a bundle changed), **on schedule** (a clock), **on demand** (a CLI subcommand). Every Effect is idempotent and re-runnable from scratch, never writes into the site root — that is the author's data (ADR-0011) — logs and retries on failure rather than dying, and leaves the engine correct if it has never run at all: a missing derivative serves the original, a missing index disables search (invariant 8). **Not an Effect:** anything computable at render time from the page plus the clock. An "this article is old" banner is a **Stage** reading `date` — right on every request, no job, no staleness, nothing written. Inbound reactions are **Interactions**. If an Effect would need to mutate content to be visible, it is a Stage wearing a disguise. **STATUS: not buildable yet.** Earned at the first derived artifact; check the Effects counter in `state.md`. Scheduling is an in-process ticker inside the single binary — no cron container and no queue until ADR-0010's second-service test is actually met. **Routing** = URL → (Bundle, View), via a resolver. **STATUS: live.** The resolver was earned at the second routing case — the default locale at the root, every other language under a prefix — and now owns URL shape so the mux has one entry. --- ## Invariants — violating one is a stop condition 1. **Open page object.** Known fields as struct members plus a `Meta`/`Extra` bag. Absence equals zero value; templates read what exists and never crash on a missing field. Never add a required field. Never make a template failure fatal at request time. 2. **Trusted / untrusted pipeline modes.** Content from the site root is trusted; everything else is not, and never reaches shortcode or template evaluation. This is the RCE boundary — the one place where an extra check beats an elegant unification. 3. **Identity is not language.** The bundle key excludes the language suffix, which is optional for the default locale (ADR-0021). Identity stays stable across translations. 4. **Request-time render behind a cache.** The server shape is the superset; static export is cache-warming, never a separate code path. 5. **Permalinks are permanent.** Renames add aliases and redirects. URLs are not reused. 6. **Interactions are off the content graph.** A comment invalidates one fragment, never a build. 7. **Every feature is a leaf** — a View, Stage, Query, Interaction adapter, Effect, or bundle-as-program, deletable without trauma. Anything wanting a permanent service or a core-model change is a **trunk** and waits for a human decision (`exploration.md`). 8. **The engine serves correctly with every external service off.** Redis, an index, object storage — each is a shortcut around work the engine can still do itself, slowly. Anything that cannot degrade that way holds canonical state and does not belong there (ADR-0010). 9. **Core stops growing after Arc 2.** Post-freeze, features arrive by composition only; core growth is a bug in the plan. Measured, not asserted: `CORE_LOC_MAX` covers `cmd/` + `internal/{content,render,web}`, `EXT_LOC_MAX` covers `internal/ext/`, reported separately by `verify.sh`. An invariant nothing measures is decoration. ## Re-render and invalidation (target shape, not built) A cached entry needs more than a content-dependency key: time, an engine/template epoch, and an opt-out all invalidate too. The shape is recorded as intent in `ideas/deferred-decisions.md`; the cache that implements it decides the fields. Two things hold regardless. Stages reach the clock only through an injected accessor, so a time-dependent render can never silently outlive its truth. And asset derivatives are content-addressed and therefore immutable — never invalidated, far-future cacheable, a different lifetime from page renders. Defer the inverted index: v1 re-renders everything, correct until it is slow. ## Dropped on purpose Guest authors. The trust model is a clean binary — site-root content trusted, everything else not — and multi-author would smear it. Comments remain the only untrusted path.