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:
+10
-91
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user