# Deferred decisions — recorded intent, not binding shape Status: parked Raised: 2026-07-30 Five ADRs were written before any code existed. They describe a direction worth keeping, but their *shape* is speculation: the harness's own rule is no abstraction before its second concrete use, and a field-level specification of an unbuilt mechanism breaks that rule in prose instead of code. Their entries in `decisions.md` are now `Status: intent` stubs pointing here, so citations still resolve and nothing is lost. Each returns as a full ADR when something implements it — and the implementation, not this file, decides the shape. ## Triggers — the one list Every deferred thing and the observation that un-defers it. **This table is the only place these triggers are written down**: `.scratch/continue.md` used to restate them and drifted, listing the extension registry as pending after ADR-0081 built it. Anywhere else that needs one links here. | Deferred | Trigger | |---|---| | Cache validity model | a render exceeding a few milliseconds, or output that stops being a pure function of content. Measured at ~63µs (ADR-0044), so not close | | Declared content types | the seventh type, the first per-type override, or a dated bundle needing to stay out of a feed. The human's "alt text and content warnings required per type" is a better trigger than any of these | | Settings cascade, section and bundle levels | the first section-level override something actually reads. The site level is built (ADR-0039). Note the coupling: `harness/state.md`'s Views counter says view selection resolves *through* the cascade, so the View layer may pull a minimal one in with it | | Feed shape, the `primary` half | declared types, which decide which types a site-wide feed carries. The tag half shipped as ADR-0032 | | Math, `$…$` → MathML | wanting it enough to accept the cost. No tagged pure-Go library exists — the only candidate is an untagged 2023 commit — and MathML can be written by hand today, since raw HTML renders (`reference/math-on-the-web.md`) | | Corrections derived from history | the Effect runner, plus a decision on whether the engine may read the content repo's local git history ([corrections-from-history.md](corrections-from-history.md)) | | Write routes — POST and friends | Arc 3. ADR-0081's seam mounts `GET` only, so the first write endpoint extends it | | Extension registry, the remaining six fields | a second implementor for whichever field. `Routes` was built at its trigger (ADR-0081); the rest have none | ## Cache validity model Status: still parked, and now with a measurement behind it (ADR-0044). Rendering a page costs ~63µs once repeated picture inspection is remembered; the parked model solves a problem the site does not have. Its trigger is a render exceeding a few milliseconds, or output that stops being a pure function of content. Deferred because no cache exists, and the harness forbids one until requests feel slow. ## Cache validity is one record with five axes Date: 2026-07-28 · Status: accepted Decision: a cache entry carries a validity record — content dependencies, interaction fragment refs, `valid-until`, engine+template epoch, and a cacheable flag — and is served only while all five hold. `valid-until` is the minimum of the windows the render's Stages declare; Stages reach the clock through an injected accessor, never `time.Now()`. An optional scheduled Effect may pre-warm entries, but scheduling is never what makes a page correct. Why: invalidation has five causes, not one. Content edits and interactions were already modelled; time (old-article banner, relative dates, future-dated publication), epoch (a template fix silently serving old HTML), and opt-out (search, a random-page route) were not. A scheduled re-render sweep expresses only the time axis, and coarsely — relative dates would need a sweep finer than their own granularity. Consequence: cheap — the record maps onto HTTP semantics (ETag from deps plus epoch, `Expires` from `valid-until`, `no-store` for opt-out), so a reverse proxy or CDN in front is correct with no extra code, and a new time-dependent Stage needs no central registry because windows compose by minimum. Expensive — the Stage contract gains a return channel, and a Stage that reads the clock without declaring a window would serve staleness silently, which is why the clock accessor is enforced by `verify.sh` rather than trusted. Revisit if: windows cannot express some invalidation cause — then add an axis, do not add a mechanism. ## Declared content types Deferred because the MVP can default a type from its section directory; a declaration file earns itself at the seventh type or the first per-type override. ## Content types are declared, not coded Date: 2026-07-28 · Status: accepted Decision: each content type is a declaration — section, default view, ordering rule, feed membership, required frontmatter, whether titleless is legal, applicable taxonomies. The binary embeds a default set (`post`, `comic`, `art`, `writing`, `page`, `status`); a declaration file in the site root extends or overrides it. Adding a type costs a declaration plus a template and no core change; an unparseable or incoherent declaration is a loud startup failure. Why: six types already carry per-type behaviour, described in prose. Prose becomes either a `switch type` in the core — forbidden, and it would grow with every new type — or unwritten template convention. A declaration is the only form that keeps the seventh type as cheap as the sixth. Consequence: cheap — a new type is a site-repo change with no engine deploy (ADR-0011), and the content `check` command gets its validation rules from the same declarations for free. Expensive — the engine must treat the type set as data, so nothing may assume a fixed list, and per-type behaviour reachable only from code (a bespoke View) still needs a template rather than a branch. Revisit if: a type needs behaviour no declaration can express — then it is a View or an Effect, not a new field on every type. ## Settings cascade Status: partly adopted → the **site** level is built as `site.yaml` (ADR-0039). The section and bundle levels stay parked: frontmatter covers a bundle, and the trigger for the rest is unchanged — the first section-level override with a reader. Deferred because frontmatter alone covers the MVP; a cascade earns itself when a section-wide policy is actually wanted. ## Settings cascade: site → section → bundle Date: 2026-07-28 · Status: accepted Decision: settings resolve down a cascade — site declaration, then each enclosing section's `_index`, then the bundle's own frontmatter — with the nearest explicit value winning. The cascade carries stage toggles, view selection, taxonomy defaults, cache flags, and metadata defaults; it carries *declared* keys only, never arbitrary engine internals. Stages apply to everything by default and are switched off by a cascade key, not by a predicate compiled into the stage. Why: stages are mostly no-ops where they do not apply, and their early return is a check on content shape ("no images here") which is type-independent and belongs in the stage anyway. The cases that really need control are narrower and cut across types — dithering wrong on one diagram, autolinking wrong in one poem — which a per-type stage set cannot express and a cascade can. It also unifies with template selection, which wants the same resolution order. Consequence: cheap — one resolution rule covers rendering, presentation and metadata defaults; a section sets a policy once for everything beneath it. Expensive — resolution must be cheap and cached, since it runs per bundle, and the set of cascadable keys must stay declared or it becomes unbounded config. Revisit if: cascade resolution shows up in a render-path profile. ## Feed shape Deferred because no feeds exist yet. The tag half of this became ADR-0032 when tags were built. Recorded direction: `/feed.xml` carries every type declared `primary`, `/{section}/feed.xml` carries a section, and `/tags/{term}/feed.xml` falls out of the same Query. Which types are `primary` is part of the type declaration, so this and declared types land together. ## Extras Status: **adopted → ADR-0047**, with the shape discovered by building it. Two things changed on contact: the directory name is fixed rather than a cascade key (nothing reads a section-level setting yet), and an entry is resolved against the enumeration rather than the filesystem. Deferred because not in the MVP, and its shape should be discovered by building it. ## Extras: local assets, enumerated and browsable Date: 2026-07-29 · Status: accepted Decision: a bundle may hold a directory of supporting files — default `extras/`, named by a cascade key — which the bundle scanner **skips entirely**: a `.md` in there is an asset, never a bundle. The engine enumerates it as a tree, classifies each entry by extension, renders the ones it can (markdown, plain text), and serves the rest as bytes behind the ADR-0024 guard. Two behaviours on one route: `…/extras/{path}` renders the listing with that entry selected, `?raw` returns the bytes. Sorted by filename; excluded from feeds, queries and search. Why: drafts, notes and logs are worth publishing as artefacts of the process, and they are not bundles — no frontmatter, no identity, no language variants. `architecture.md` already defines a Bundle as carrying local assets; the only thing missing was enumerating them instead of merely referencing them relatively. Consequence: cheap — no new primitive, and selecting an entry is an ordinary link, so the whole feature works without JavaScript. Expensive — the scanner needs an exclusion rule it did not have, and the extras segment becomes a name no child of a bundle may use, which is why it is a cascade key rather than a constant. Revisit if: extras need per-file metadata — and then they are bundles after all, and this entry was wrong.