harness: withdraw five pre-code ADRs to deferred ideas

Twenty-seven ADRs existed before a line of Go. Five specified the shape of
unbuilt mechanisms — cache validity, declared types, the settings cascade,
taxonomies, extras — which breaks the rule against abstraction before a
second concrete use, in prose where the counters cannot see it. They move to
ideas/deferred-decisions.md as recorded intent and return shaped by whatever
implements them.

Citations retargeted throughout; where one was decoration the rule now stands
on its own reasoning. Type declarations and the cascade drop to [spec] with
the MVP behaviour stated instead, so the first prompts have less to build.
conventions.md names http.ServeMux as the router, closing a hole that invited
hand-rolling a path splitter. The ADR gate now checks a number is registered
in the log rather than headed by an entry, so withdrawals resolve and invented
numbers still fail. Two architecture invariants corrected: identity no longer
implies a required language suffix, and the duplicated permalink clause is gone.
This commit is contained in:
Claude Opus 5
2026-07-30 01:01:38 +06:00
committed by bdeshi
parent 02268f9121
commit 99f8c730b0
13 changed files with 180 additions and 144 deletions
+10 -91
View File
@@ -1,6 +1,13 @@
# Decisions (ADR log)
Append-only. Never rewrite history; supersede with a new entry. Six lines each:
Append-only. Never rewrite history; supersede with a new entry. Six lines each.
Withdrawn: ADR-0013, ADR-0014, ADR-0017, ADR-0018, ADR-0025 — recorded pre-code, now intent in
`ideas/deferred-decisions.md`.
Numbers are never reused. A gap means an entry was withdrawn to `ideas/deferred-decisions.md` — recorded
before any code existed, so its shape was speculation rather than a decision. Do not add a pre-code ADR
for a mechanism nothing implements; write it as an idea and let the implementation decide the shape.
```
## ADR-NNNN — Title
@@ -136,42 +143,6 @@ means the engine has background work, so every Effect must be idempotent and its
rather than break. Anything requiring a separate scheduler process is a trunk under ADR-0010.
Revisit if: an Effect cannot be made idempotent, or scheduling genuinely needs a second process.
## ADR-0013 — 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.
## ADR-0014 — 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.
## ADR-0015 — Normalise to NFC everywhere; derive slugs by locale, override by hand
Date: 2026-07-28 · Status: accepted
Decision: every string entering the engine as an identifier — filenames, bundle keys, taxonomy terms,
@@ -206,46 +177,12 @@ Expensive — ordering is invisible in a directory listing, so authors read it f
`order` values want leaving gaps.
Revisit if: never. Position in a permalink is the mistake this exists to prevent.
## ADR-0017 — 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.
## ADR-0018 — 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.
## ADR-0019 — Templates: per-type sets, block-level override, cascade selection
Date: 2026-07-28 · Status: accepted
Decision: one parsed template set per type, each set being base plus partials plus that type's
definitions. A site root override is parsed *after* the embedded defaults into the same set, so it may
redefine a single named block and inherit everything else. Which set renders a bundle resolves through the
cascade (ADR-0017): type default, section override, then the bundle's own `view`.
cascade (`ideas/deferred-decisions.md`): type default, section override, then the bundle's own `view`.
Why: `html/template` has no `extends` — inheritance is "last definition of a name wins in a parsed set",
so a global set makes two types defining `main` collide, and per-type sets are the only clean answer.
File-level override would force copying a whole template to change one block, after which it stops
@@ -271,7 +208,6 @@ multi-line strings work as authors expect. Expensive — the third allowlist ent
Revisit if: the parser proves a maintenance burden, or the author-facing format changes — and the second
requires migrating every existing file.
## ADR-0021 — The default locale's suffix is optional, permanently
Date: 2026-07-28 · Status: accepted (supersedes ADR-0004's instruction to adopt `slug.en.md` from day one)
Decision: a missing language suffix means the default locale, always — not merely while one language
@@ -336,30 +272,13 @@ unfinished work leak through the asset path — which looks like static file ser
looks like it needs no context. That is the shape this bug always takes. 403 would confirm the work exists,
which for drafts is itself the thing worth not leaking.
Consequence: cheap — visibility is one derived predicate with no state to keep in sync, and a future-dated
bundle's 404 carries `valid-until` = its publish time (ADR-0013), so it expires exactly when it should
bundle's 404 expires at its publish time, so it becomes visible exactly when it should
rather than needing a sweep. Expensive — no route may serve bundle bytes by path alone, so a fast static
path for assets is off the table; and `-dev` becomes security-relevant, so it must default off and be
obvious when on.
Revisit if: never. The generalisation from "extras are public" to "assets inherit visibility" is the whole
point of the entry.
## ADR-0025 — 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.
## ADR-0026 — The engine ships a minimal reference theme
Date: 2026-07-29 · Status: accepted
Decision: the binary embeds a reference theme — templates plus one small stylesheet — sufficient to render