# 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. ## Cache validity model 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 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. ## Taxonomies and feed shape Deferred because no tag pages and no feeds in the MVP. ## Global flat tags, declared structural taxonomies Date: 2026-07-28 · Status: accepted Decision: `tags` is one global namespace across every type — `/tags/{tag}/` lists everything carrying it, `/{section}/tags/{tag}/` narrows to a section, and listing views group results by type. Structural metadata with known terms that drives behaviour — `series`, `medium`, `genre` — is a *declared* taxonomy on the type (ADR-0014) and never enters the tag pool. Feeds follow the same shape: `/feed.xml` carries every type declared `primary`, `/{section}/feed.xml` carries a section, `/tags/{tag}/feed.xml` falls out of the same Query. Why: cross-type discovery is the point of a single-author site — one tag spanning a comic, a poem and a photo essay is a feature. Per-section tag pools would fragment that for a readability problem better solved by grouping in the View. But a tag is free-form and cross-cutting, while a structural taxonomy has a fixed term set and changes what the engine does; conflating them makes both worse. Consequence: cheap — one Query with an optional section predicate serves tag pages, section tag pages and their feeds. Expensive — tag hygiene is now the author's discipline, since nothing scopes them; the content `check` command should report near-duplicate terms. Revisit if: the tag pool becomes unusable in practice — and then the answer is curation, not namespacing. ## Extras 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.