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:
@@ -39,6 +39,7 @@ you, not the agent; `verify.sh` keeps it honest without reading it into context.
|
||||
|
||||
## Index
|
||||
|
||||
- [deferred-decisions.md](deferred-decisions.md) — five pre-code ADRs demoted to intent; each returns when something implements it. **parked**
|
||||
- [specs-as-secondary-artifacts.md](specs-as-secondary-artifacts.md) — optional per-feature specs, derived by default, plus the named-test convention. **parked**
|
||||
- [engine-design-review.md](engine-design-review.md) — open design decisions for a multi-type site; items graduate to ADRs one at a time. **parked**
|
||||
- [token-conservation.md](token-conservation.md) — cut agent token cost without losing output quality. **parked**
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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.
|
||||
@@ -6,9 +6,9 @@ Raised: 2026-07-28
|
||||
One review, many decisions. Each item below graduates independently: when one is settled it becomes an
|
||||
ADR (or a change to its owning doc) and is struck from this list. When the list empties, delete the
|
||||
file. Graduated so far: site root external to the engine repo (ADR-0011), Effect as the sixth
|
||||
primitive (ADR-0012), the cache validity model (ADR-0013), declared content types (ADR-0014),
|
||||
NFC normalisation and slug derivation (ADR-0015), sequence position (ADR-0016), the settings cascade
|
||||
(ADR-0017), taxonomies and feeds (ADR-0018), template composition (ADR-0019), feature locality (ADR-0027).
|
||||
primitive (ADR-0012), NFC normalisation and slug derivation (ADR-0015), sequence position (ADR-0016),
|
||||
template composition (ADR-0019), feature locality (ADR-0027). Five more were recorded and then deferred
|
||||
to `deferred-decisions.md`: cache validity, declared types, the cascade, taxonomies, extras.
|
||||
|
||||
## Why it came up
|
||||
|
||||
|
||||
Reference in New Issue
Block a user