Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:34:18 +06:00
co-authored by Claude Opus 5
commit 02268f9121
33 changed files with 2730 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# 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 the settings cascade (ADR-0017), 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: deferred until the first collection page.** One-off directory reads are fine before then.
## 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: buildable, inline.** Keep cases inline;
extract the resolver when the routing counter in `state.md` is due, not before.
---
## 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.** `slug.bn.md` / `slug.en.md` with a defined fallback, even while one
language exists. Identity stays stable across translations.
4. **Request-time render behind a cache, fixed permalink policy.** 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)
One validity record per cached entry, five axes, served only while all hold (ADR-0013):
| Axis | Invalidated by |
|---|---|
| content deps | bundle keys plus query signatures, taking `old new` for membership changes |
| interaction fragments | a comment or webmention arriving — the fragment, never the page (invariant 6) |
| `valid-until` | the clock passing the minimum window the render's Stages declared |
| epoch | an engine or template change, which stales every entry at once |
| cacheable | the bundle or route declaring itself uncacheable |
The record maps onto HTTP: ETag from deps plus epoch, `Expires` from `valid-until`, `no-store` for
opt-out — so a proxy in front stays correct without engine code. Stages reach the clock only through an
injected accessor, so a window can never be silently forgotten. Defer the inverted index — v1
re-renders everything, correct until it is slow. Asset derivatives are content-addressed and therefore
immutable: never invalidated, far-future cacheable, a different lifetime from page renders.
## 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.