Files
khosra/ideas/deferred-decisions.md
T
bdeshi 581d6c08ee 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.
2026-08-01 02:23:33 +06:00

8.2 KiB

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.