Held on the human's call: the code move waits for the idea review, since how much
core room is needed depends on what that review places in core.
Worth recording rather than rediscovering. Core sits at ~35 lines of headroom with
at least four core-bound items before the Arc 2 freeze — the View layer, the Stage
pipeline (roadmap.md Arc 2 item 2, which I had been treating as optional), a
minimal settings cascade the Views counter couples to view selection, and declared
content types.
The relief I recommended was overstated and is corrected here. discover.go (~71
lines) can leave core once ADR-0081's seam passes the live index. feed.go and
web/extras.go cannot: a feed lives at /{section}/feed.xml and extras under a
bundle's own URL, so both are resolver cases while the seam mounts exact paths
only — the limit that ADR's own "revisit if" line predicted and I did not check
before recommending ~196 lines of relief.
So the open choice is extending the seam to resolver participation versus raising
CORE_LOC_MAX with an ADR, and it is the review's to settle.
2 files. No rule or threshold moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
10 KiB
Markdown
137 lines
10 KiB
Markdown
# 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.
|
|
|
|
## Triggers — the one list
|
|
|
|
Every deferred thing and the observation that un-defers it. **This table is the only place these triggers
|
|
are written down**: `.scratch/continue.md` used to restate them and drifted, listing the extension registry
|
|
as pending after ADR-0081 built it. Anywhere else that needs one links here.
|
|
|
|
| Deferred | Trigger |
|
|
|---|---|
|
|
| Cache validity model | a render exceeding a few milliseconds, or output that stops being a pure function of content. Measured at ~63µs (ADR-0044), so not close |
|
|
| Declared content types | the seventh type, the first per-type override, or a dated bundle needing to stay out of a feed. The human's "alt text and content warnings required per type" is a better trigger than any of these |
|
|
| Settings cascade, section and bundle levels | the first section-level override something actually reads. The site level is built (ADR-0039). Note the coupling: `harness/state.md`'s Views counter says view selection resolves *through* the cascade, so the View layer may pull a minimal one in with it |
|
|
| Feed shape, the `primary` half | declared types, which decide which types a site-wide feed carries. The tag half shipped as ADR-0032 |
|
|
| Math, `$…$` → MathML | wanting it enough to accept the cost. No tagged pure-Go library exists — the only candidate is an untagged 2023 commit — and MathML can be written by hand today, since raw HTML renders (`reference/math-on-the-web.md`) |
|
|
| Corrections derived from history | the Effect runner, plus a decision on whether the engine may read the content repo's local git history ([corrections-from-history.md](corrections-from-history.md)) |
|
|
| Write routes — POST and friends | Arc 3. ADR-0081's seam mounts `GET` only, so the first write endpoint extends it |
|
|
| Features participating in URL *resolution* | wanting to move `feed.go` or `web/extras.go` out of core. ADR-0081 mounts exact mux paths, which is why `discover.go` can leave core and those two cannot: a feed lives at `/{section}/feed.xml` and extras under a bundle's own URL, so both are resolver cases rather than fixed names |
|
|
| Extension registry, the remaining six fields | a second implementor for whichever field. `Routes` was built at its trigger (ADR-0081); the rest have none |
|
|
|
|
## Cache validity model
|
|
|
|
Status: still parked, and now with a measurement behind it (ADR-0044). Rendering a page costs ~63µs once
|
|
repeated picture inspection is remembered; the parked model solves a problem the site does not have. Its trigger
|
|
is a render exceeding a few milliseconds, or output that stops being a pure function of content.
|
|
|
|
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
|
|
|
|
Status: partly adopted → the **site** level is built as `site.yaml` (ADR-0039). The section and bundle levels
|
|
stay parked: frontmatter covers a bundle, and the trigger for the rest is unchanged — the first section-level
|
|
override with a reader.
|
|
|
|
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.
|
|
|
|
## Feed shape
|
|
|
|
Deferred because no feeds exist yet. The tag half of this became ADR-0032 when tags were built.
|
|
|
|
Recorded direction: `/feed.xml` carries every type declared `primary`, `/{section}/feed.xml` carries a
|
|
section, and `/tags/{term}/feed.xml` falls out of the same Query. Which types are `primary` is part of the
|
|
type declaration, so this and declared types land together.
|
|
|
|
## Extras
|
|
|
|
Status: **adopted → ADR-0047**, with the shape discovered by building it. Two things changed on contact: the
|
|
directory name is fixed rather than a cascade key (nothing reads a section-level setting yet), and an entry is
|
|
resolved against the enumeration rather than the filesystem.
|
|
|
|
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.
|