give pending work four owners instead of six scattered lists

Ideas were spread across six places with real duplication and one list that was
about to be lost. Consolidated by merging the duplication, not the categories.

The 130-item feature-list triage lived in .scratch/, which is gitignored — 130
verified verdicts a scratchpad cleanup would have deleted. It is now
ideas/feature-list-triage.md with the header the ideas gates require, indexed,
and out of agent context by default like everything there. exploration.md's
Verdicts table was empty and designed for exactly this; it now carries one row per
body of work with a pointer, rather than 130 rows nobody could skim.

Deferred triggers had no single owner: continue.md restated them and drifted,
still listing the extension registry as pending after ADR-0081 built it.
deferred-decisions.md now holds the only trigger table, covering the two it
already described plus math, corrections, write routes and the registry's
remaining fields. continue.md links to it. Two rows record couplings worth not
rediscovering: the View layer may pull a minimal settings cascade in with it,
because the Views counter says selection resolves *through* the cascade; and
ADR-0081's seam mounts GET only, so Arc 3's first write endpoint extends it.

Corrections per post is dropped in its cheap form and parked in its real one. A
hand-maintained frontmatter list was rejected for an authoring reason rather than
a technical one — a half-remembered list of corrections misleads where none would
not, because two entries imply those were the only two. Deriving it from
repository history is the shape worth waiting for, and the file records what must
be settled first: whether the engine may read local git history at all, that it
needs a dependency or a subprocess, and that not every commit is a correction.

Not merged, on purpose: latent items stay in state.md because they describe
shipped code and are gated against .go commits; arcs stay in roadmap.md because
they are sequencing decisions. One file would have flattened four levels of
authority and made pending work cheap to load, which is the opposite of why
ideas/ sits outside harness/ at all. harness/README.md now says which list owns
what, in both the topic table and the single-source table.

7 files. No rule or threshold moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 23:31:24 +06:00
co-authored by Claude Opus 5
parent 16fde5ee68
commit 949ce34224
6 changed files with 441 additions and 2 deletions
+2
View File
@@ -43,6 +43,8 @@ you, not the agent; `verify.sh` keeps it honest without reading it into context.
- [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**
- [exploration.md](exploration.md) — the catalog of engine features nobody has asked for, with leaf/trunk verdicts. **catalog**
- [feature-list-triage.md](feature-list-triage.md) — the human's 130-item feature list, each with a verdict and its justification. **catalog**
- [corrections-from-history.md](corrections-from-history.md) — why the cheap corrections field was rejected, and the derived shape worth waiting for. **parked**
- [token-conservation.md](token-conservation.md) — cut agent token cost without losing output quality. **adopted → `harness/context-economy.md`**, except the subagent question
It lives here rather than in `harness/` because a catalogue of things nobody has asked for is storage, not
+48
View File
@@ -0,0 +1,48 @@
# Corrections and updates, derived rather than remembered
Status: parked
Raised: 2026-08-02
A post that has been corrected should say so. The question is who remembers.
## Why the obvious version was rejected
The cheap shape is a frontmatter list — `corrections: [{date, note}]` — lifted by the parser and rendered
by the theme. It needs no engine history at all and could ship in an afternoon.
It was dropped for an authoring reason, not a technical one: **it puts the burden on the author to
remember, and a half-remembered list of corrections is worse than none.** A page carrying two entries
implies those were the only two, which is a stronger claim than the author can honestly make months later.
An absent list claims nothing; an incomplete one misleads. That asymmetry is what kills it.
## The shape worth waiting for
Derive it. The repository already knows what changed and when — that is what a content repo is for — so the
list should come from history rather than from memory.
This wants the **Effect** primitive: work off the request path, on content change or on a schedule
(ADR-0012), writing an artifact the render then reads. An Effect walking the content repo's history for a
bundle's path can produce a real, complete list of when that file changed, with commit subjects as the
notes.
## What has to be settled first
- **The engine does not read git today, deliberately.** `harness/content-model.md`: "The engine notices
changes. It never fetches them — pulling a git repository is the operator's business." That sentence is
about *pulling from a remote*, which is a different act from reading local history, but the distinction
has never been decided and should not be assumed.
- **Reading history needs a dependency or a subprocess.** Either a Go git library — an ADR and an
allowlist entry — or shelling out to `git`, which makes the binary's behaviour depend on a tool that may
be absent in the container (ADR-0010 ships the binary alone).
- **Not every commit is a correction.** A typo fix, a reflow and a retitle are all commit subjects, and
publishing them as corrections recreates the noise problem from the other direction. Something has to
filter — a commit-message convention, a trailer, or a path-scoped rule — and that choice is the actual
design work here.
- **The Effect runner does not exist yet.** The effects counter stands at 1, due at 2
(`harness/state.md`), so this waits on that extraction regardless.
## Trigger
The Effect runner landing, plus a decision on whether the engine may read the content repo's local
history. Until both, this stays parked — and the manual version stays rejected rather than merely
unbuilt, so it is not reintroduced as an easy win.
+17
View File
@@ -11,6 +11,23 @@ Their entries in `decisions.md` are now `Status: intent` stubs pointing here, so
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 |
| 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
+7 -2
View File
@@ -18,9 +18,14 @@ Use `/leaf <topic>` to get a verdict written into this file.
## Verdicts
| Item | Verdict | Reduces to | Note |
One row per body of work triaged, not per item — the item-level verdicts live in the file each row names,
because 130 rows here would make a catalogue nobody can skim.
| Body of work | Verdict | Reduces to | Note |
|---|---|---|---|
| (empty — filled by `/leaf`) | | | |
| The human's feature list, 130 items | triaged in full | mostly nothing new | [feature-list-triage.md](feature-list-triage.md): 42 BUILT, 24 THEME (a fragment or CSS, no engine code), 29 LEAF, 2 TRUNK, 4 PARK. Three LEAF items have shipped since — task lists (ADR-0078), page assets (ADR-0079), passthrough routes (ADR-0081) |
| Corrections and updates per post | **rejected in its cheap form**, parked in its real one | Effect | [corrections-from-history.md](corrections-from-history.md). A hand-maintained frontmatter list was dropped because a half-remembered list of corrections misleads where none would not; deriving it from repository history waits on the Effect runner and on whether the engine may read git at all |
| Per-piece template variants | the highest-value item in the list | **View** | Not parked — it is the second concrete use that earns the View primitive, and `harness/roadmap.md` closes Arc 2 when that lands. Tracked as scheduled work, not as an idea |
## Catalog
+365
View File
@@ -0,0 +1,365 @@
# Feature-list triage
Status: catalog — one verdict per item, overridden by the answers at the top
Raised: 2026-08-02
Every item from the human's feature list, with a verdict. Not a plan and not a queue — nothing here has
authority over the code, and being listed earns nothing. It lives in `ideas/` because that is where work
nobody has committed to belongs, and it is out of agent context by default like everything else here.
Summary counts, which `ideas/exploration.md` also carries: 130 rows — 42 BUILT, 24 THEME, 29 LEAF, 2 TRUNK,
4 PARK, 4 CONFLICT (all four now resolved). Three of the LEAF items have since shipped.
Verified against the code on 2026-08-02, not against `state.md`. Where a verdict says BUILT it means the
code does it, and the citation is where to look.
**Detail volume was normalised deliberately.** The longest section (Content-specific) is mostly frontmatter
that already works plus one genuine primitive; the shortest line in the whole list (*"Search"*) is the
largest single piece of work in it.
## Answered 2026-08-02 — these override the verdicts below
1. **Categories: sections are categories.** No second taxonomy. Every "categories" row below resolves to
BUILT, and the tag axis stays the only non-structural one.
2. **Private: dropped entirely.** No secret links, no capability URLs — ADR-0024 stands untouched. `draft`
plus **unlisted** covers the ground, so unlisted survives as a small LEAF and `private`/`archived` leave
the list.
3. **Task lists: enabled, superseding ADR-0058's exclusion.** The `TaskList` extension only. Linkify and the
rest of the GFM bundle stay out — linkify rewrites an author's plain text into markup, which is the line
ADR-0034 draws and is a separate decision from this one.
4. **JS: a small page-scoped exception is allowed.** Opt-in per page via frontmatter, never site-wide, which
requires the `styles`/`scripts` fields to be actually lifted. The antifeature list must be written down
*with* the exception in it — an exception nobody recorded is how the whole list erodes.
5. **Git sync stays out of the engine.** `content-model.md`'s position is untouched: something else pulls,
the engine notices the changed files and rebuilds, which already works. No credentials in the engine and
no write path to the content repo. The "incoming webhook" half remains an Arc 3 question, not a git one.
6. **Shared parts between bundles: left blocked, deliberately.** Not worth a permanent disk-contract
decision yet. So cross-document jargon tooltips and the worldbuilding glossary are **PARK**, and
abbreviations stay per-document as ADR-0062 has them. Revisit when something being written needs it.
7. **The Extras section is live scope**, not a remember-these-exist list — the human overrode their own
header. Its verdicts below stand as real candidates.
**The decided small batch is done** (`69f43e5`, `64e53f2`): task lists enabled superseding ADR-0058, the
antifeature list recorded as ADR-0080 with its one exception, and page assets shipped as ADR-0079 — theme
`assets:<name>` fragments pulled in by the shortcodes that need them or by `use:`, plus the bundle's own
`styles`/`scripts`, lifted at last. Unlisted was dropped. So every verdict below tagged **THEME** for a
shortcode needing CSS or JS is now genuinely reachable without engine work.
Unstarted, in no fixed order: the View layer (closes Arc 2), raw passthrough routes, corrections/updates
per post.
Still open: whether to fix `SKILL.md`'s stale three-question cap (`CLAUDE.md` §4 uncapped it in `25d7045`);
declared content types (queue 7), which is what "alt text and content warnings first-class per type" needs.
## Verdict key
| Tag | Means |
|---|---|
| **BUILT** | Ships today. Cited. |
| **THEME** | A theme fragment, template or CSS. No engine code, no rebuild — proven below. |
| **LEAF** | Engine work that reduces to an existing primitive. Buildable when wanted. |
| **TRUNK** | Wants a permanent service or a core-model change. Needs a deliberate decision in an ADR. |
| **PARK** | Real but not now. `ideas/exploration.md`. |
| **CONFLICT** | Contradicts a recorded decision. Surfaced, never resolved silently (`CLAUDE.md` rule 9). |
| **?** | Needs an answer before it can be classified. |
## The one verification that reframes the list
A shortcode name the engine has never heard of renders from a theme fragment alone, and the fragment
hot-swaps while the server runs:
```
::spoiler{summary="Chapter 3 twist" body="She was the lighthouse."}
+ templates/shortcodes/spoiler.html → renders, no engine code, no rebuild
edit the fragment with the server up → new output in one poll interval
```
The parser's `switch` on name only *enriches* three names (`gallery` reads the directory, `figure`
inspects a picture, `include` marks content). Every other name becomes a node and renders through a
fragment of that name; a name the theme does not define renders nothing and logs it
(`internal/ext/shortcodes/shortcodes.go`, `content-model.md` Shortcodes).
So **"lightweight shortcode mechanism, loaded as data/templates rather than hardcoded, so I can add one
without a rebuild" is already built exactly as specified** — and any item that is "render this markup from
these arguments" is a fragment you write, not a feature I build.
---
## Content
| Item | Verdict | Justification |
|---|---|---|
| post / page / note / project as separate types | **BUILT** as convention | Sections are the type: `content-model.md` Post types already names post/comic/art/writing/page/status, defaulting from the top-level directory. `note/` and `project/` are two `mkdir`s. A *declared* type file with per-type defaults is deferred (queue 7, `ideas/deferred-decisions.md`) and waits for a dated bundle needing to stay out of a feed |
| Tags | **BUILT** | Flat, case- and script-preserved, slugged for URLs (ADR-0018); listings global and section-narrowed, paginated, with per-tag Atom feeds (ADR-0043) |
| Categories | **?** | See question 1. My read: sections already *are* categories — one per post, structural, in the URL. A second single-select axis buys a routing case and a listing shape you already have |
| Series with its own ordered index | **BUILT** | `Sequence`: structural membership by nesting (ADR-0033), `order` ascending sparse, prev/next/first/last/count, series archive on any nested bundle, resolving through language fallback (ADR-0016) |
| Page status: draft | **BUILT** | Lifted to `Bundle.Draft`; 404 for the bundle *and* every file inside it (ADR-0024); `-dev on` reveals. Future-dating works the same way and expires per request |
| Page status: unlisted | **LEAF**, small | Served at its URL, absent from listings, feeds and sitemap. No conflict — it is a query-membership flag, and `Site.Run` is already the one place membership is decided |
| Page status: private via secret link | **CONFLICT** | ADR-0024 chose 404 over 403 for unpublished work *specifically so existence is not confirmed*. A secret link serves unpublished content to whoever holds the URL, which is a capability-URL design (unguessable token, no listing, no sitemap, `noindex`) and a different security model. Question 2 |
| Page status: archived | **THEME** | A frontmatter flag the template reads. Nothing engine-side unless it changes query membership, in which case it is `unlisted` above |
| Page inclusion — embed one fragment in another | **BUILT** | `::include{file=…}`; merging is the default so the page is one document (ADR-0066, ADR-0076), `include: embed` for containment. One level, no `..`, missing file logs and still serves |
| Per-page custom CSS/JS for demos | **THEME today**, LEAF to formalise | `styles`/`scripts` are in `content-model.md`'s table but **not lifted** — verified: nothing in `internal/` reads them, so they sit in `Extra`, which the theme can already read (ADR-0002 open page object). Lifting them properly is a small LEAF. Consistent with your antifeature: JS for demos only |
| Folder-per-post browsable extras | **BUILT**, precisely as described | `extras/` (ADR-0047): scanner skips it, enumerated as a sorted tree, classified `markdown/text/image/pdf/audio/video/other/dir`, markdown and text rendered, everything else offered as bytes, `?raw` for the file itself, excluded from queries/feeds/sitemaps. Your "old drafts, notes, worldbuilding material, reference lists, code samples" is the exact use it was built for |
| Inline image alignment / sizing | **THEME** | The `figure` fragment plus CSS classes. `left/right/center/full-width` is layout, and ADR-0045 is the precedent for not doing layout in the engine |
| — cropped | **LEAF** | The one part that is not CSS: derivatives are width-only today (ADR-0042). A crop is a new operation on the existing pass |
| EPUB export | **LEAF** | Genuinely new. An Effect writing an artifact: `archive/zip` and XHTML are stdlib, so no dependency. Wants the Effect runner (counter at 1, due at 2) |
| Server-rendered QR code | **LEAF** | Self-contained, no client JS, fits the values. ~200 lines hand-rolled, or a dependency needing an ADR. Nice small win |
## Authoring
| Item | Verdict | Justification |
|---|---|---|
| Markdown + footnotes, tables | **BUILT** | CommonMark plus tables, footnotes, definition lists, heading ids declared or derived (ADR-0058). Footnote ids namespace per included file |
| — task lists | **CONFLICT** | ADR-0058 excluded them *deliberately*: "a note-taking affordance, not a publishing one", and the GFM bundle was refused because it drags task lists and linkify in with the tables. Question 3 |
| Frontmatter fields | **BUILT** | Unknown keys land in `Extra` and templates read them (ADR-0002). Most of the Content-specific section below needs nothing more than this |
| TOC generation | **BUILT** | `::toc`, `::toc{depth=2}`; entries carry level, text without markup, and the id the engine assigned, so indentation is CSS (ADR-0065, ADR-0066) |
| Reading time, word count | **LEAF**, tiny — and notable | These are the first *page-level* transforms, the same slot as OpenGraph/JSON-LD (queue 15). The transform counter is at 0 of 3, so this is one of the three things that would earn the Stage pipeline |
| Image galleries | **BUILT** | `::gallery` lists pictures beside the bundle in filename order; subdirectories and unshowable files excluded (ADR-0016) |
| oEmbed-style embeds | **TRUNK**, and your own list answers it | The engine never fetches — `content-model.md`: "It never fetches them. Pulling a git repository is the operator's business". Also collides with your antifeature about third-party embeds. Your "static self-rendered copies" is the right answer; see below |
| Typography: smart quotes, dashes | **BUILT** | goldmark typographer on the parsed tree, so code spans keep straight quotes and `--` (ADR-0034) |
| — widow/orphan control | **CSS, and settled** | You already say "mostly CSS", and you are right for a recorded reason: ADR-0045 *deleted a shipped engine feature* for this. `text-wrap: pretty`/`balance` in `theme.css` does it |
| Lightweight shortcode mechanism, no rebuild | **BUILT** | Verified this session, see above. This is the highest-value already-built finding in the list |
| Timeline within a single page | **THEME** | A container shortcode over authored entries. Zero engine code |
| Timeline across pages / series | **LEAF** | A Query (already earned) plus a listing shape. The genuinely new half of that bullet — worth splitting from the other |
| admonitions, info boxes, tabs | **BUILT** | `:::note`, `:::warn`, `:::tip`, `:::details` — and siblings sharing a `group` render as tabs — plus `:::aside` for margin notes (ADR-0064). A kind the theme does not define renders its body unwrapped rather than losing it |
| Static self-rendered copies of external content | **Reframe → THEME** | As an engine fetch it is a TRUNK (see oEmbed). As *content* it is free: an external tool snapshots the tweet into the bundle, and a fragment renders it. Same reader benefit, no outbound capability, no trust boundary. Strong recommendation |
| Inline code highlighting via a marker | **LEAF**, small | Block highlighting is built (chroma, ADR-0075). Caution: your proposed `` `{lang=python}…` `` is exactly the "what prose does this eat?" failure mode — five defects this project has already shipped were new syntax swallowing ordinary text |
| Ruby annotations | **LEAF**, small | An inline mark in `internal/ext/notation/`, which owns exactly this (ADR-0061), or a fragment if you accept a shortcode call instead of terse syntax |
| Strikeout and highlight | **BUILT** | `~~struck~~` and `==marked==`, khosra's own so a single tilde can mean subscript (ADR-0061) |
| Columnar layout | **THEME** | CSS columns |
| Archive.org link beside external links | **split** | Rendering the link: **THEME**. Auto-submitting on publish: **LEAF** after the Effect runner, and outbound to a third party |
| Readability score EN/BN | **PARK** | English is easy. Bengali is not, and `roadmap.md` flags Bengali tokenisation as the genuinely novel problem in this project — do not solve it as a side effect of a stats page |
## Multilingual
| Item | Verdict | Justification |
|---|---|---|
| EN/BN pairing between translations | **BUILT** | Language variants per bundle key, fallback requested → default → any → 404 (ADR-0004), English at root and `/bn/` prefix with `/en/…` redirecting (ADR-0009) |
| Script-aware typography per language | **BUILT** (chrome) + **CSS** (body) | Chrome, dates and digits localise from an engine table — `পৃষ্ঠা ২ / ২`, `৮ মার্চ ২০২৬` (ADR-0034). Authored text is deliberately never localised. Per-script *typography* is CSS, and the `lang` the theme needs is already on the fragment (ADR-0067) |
| `hreflang` | **BUILT** | Emitted from the variants that exist, absolute once `base` is declared (ADR-0039) |
## Theming
| Item | Verdict | Justification |
|---|---|---|
| Partials / includes | **BUILT** | Fragments and `Partial` in the theme contract; shortcode fragments may be one file or a directory (ADR-0071) |
| Dark / light via `prefers-color-scheme` | **BUILT** | Present in `theme.css` — verified |
| Design tokens instead of hardcoded CSS | **THEME** | Custom properties. `theme.css` is yours to restructure; the engine has no opinion |
| Print stylesheet | **THEME** | Not written. Pure CSS |
## Discovery
| Item | Verdict | Justification |
|---|---|---|
| Archive pages for tags | **BUILT** | Global and section-narrowed tag listings, paginated at `/{section}/page/N/` (ADR-0028) |
| Search | **LEAF — and the largest item in this list** | One word, arc-sized. `roadmap.md`: "Bengali tokenisation for search is the genuinely novel problem here — solve it well and deliberately, not as a side effect of adding search." Needs an index (an Effect), a query path, and a real decision about conjunct normalisation beyond NFC |
| RSS / Atom feeds | **BUILT** | Atom per site, section and tag, from dated bundles via one Query (ADR-0043) |
| JSON Feed | **LEAF**, tiny | One more marshaller. Note `state.md`: output formats deliberately do **not** count toward the Views counter — three functions with nothing to share |
| Sitemap | **BUILT** | Every bundle in every language it exists in, `lastmod` where dated, 404 without `base` (ADR-0039) |
## SEO
| Item | Verdict | Justification |
|---|---|---|
| OG tags | **BUILT** | Emitted from fields already on the page, absolute with `base` |
| Meta descriptions | **THEME** | From `summary` or derived. No new disk field — `content-model.md` is explicit that SEO adds no frontmatter |
| Twitter cards, JSON-LD | **LEAF** — the Arc 2 mover | `content-model.md`: "no consumer yet". These are queue 15, the first page-level transforms, and they take the transform counter off 0 toward 3. Also the trigger for the latent item that the counters have no mechanical check |
| Canonical URLs | **BUILT** | Canonical plus trailing-slash canonicalisation, one rule applied once |
| Redirect handling | **BUILT** | `aliases` → permanent redirects; ambiguity logged and dropped rather than fatal (ADR-0008, ADR-0029) |
| Content as data — text / JSON / YAML | **LEAF** | Output formats. Same feature as "Personal JSON API" in Extras — one build, two list entries |
## Performance
| Item | Verdict | Justification |
|---|---|---|
| Cache | **Deliberately not built** | "No cache until requests feel slow." A whole page renders in ~63µs (ADR-0044), and a latent item explicitly declines to memoise sequence resolution for want of a measurement. Adding one now would be a cache with no number behind it |
| Static export | **LEAF** | Useful independently of caching — it is a deploy artifact and an offline copy. Reduces to Query + Effect |
| Responsive images | **BUILT** | Three widths, never upscaled, content-hash names, `srcset` closed by the original, `width`/`height` from the source so the box is reserved (ADR-0040, ADR-0042) |
| Native lazy loading | **THEME** | One attribute in the `figure`/`gallery` fragment |
| Incremental rebuilds | **PARK** with a trigger | The rebuild is a whole-index swap and is not what costs anything. Revisit when a rebuild is *felt* — the same trigger that would revive the parked cache |
## Routing
| Item | Verdict | Justification |
|---|---|---|
| Raw passthrough routes, incl. `.well-known/*` | **LEAF — highest leverage small item in the list** | `static/` only answers under `/static/`; nothing can be served *at* `/pubkey` or `/.well-known/security.txt`. This one feature satisfies four separate entries: `/pubkey`, `humans.txt`, `security.txt`, and the PGP page. Needs care: the root is engine-owned (ADR-0008, ADR-0050), so it wants an explicit reserved-path story rather than a catch-all |
| Short / alias URLs with redirects | **BUILT** | `aliases`, any number, permanent redirects, ambiguity logged and dropped |
## Events
| Item | Verdict | Justification |
|---|---|---|
| Hook points (on publish, on webmention) | **LEAF** | The Effect primitive. Counter at 1 (the derivative pass), runner due at 2 — so the second Effect earns the trigger wiring rather than inventing it |
| Outgoing webhooks (Slack, Telegram, custom) | **LEAF** after the runner | Outbound HTTP with per-target rules. Sequence it after the runner exists or it becomes the runner |
| Incoming webhooks (git push → rebuild) | **Arc 3** | A write endpoint, therefore the untrusted boundary (ADR-0003) and rate limiting. Not a small addition to a read-only server |
| Auto-submit external links to archive.org | **LEAF** after the runner | Outbound Effect, third-party dependency at runtime |
| Git pull / push sync | **CONFLICT** | `content-model.md`: "The engine notices changes. It never fetches them — pulling a git repository is the operator's business, not the engine's." A deliberate position, and reversing it means the engine holds credentials and can rewrite the author's data. Question in chat |
| Auto-flag old pages | **THEME — free today** | And it is `roadmap.md`'s own worked example: "An 'old article' banner is a Stage; it needs no job and can never be stale." Computable from the page plus the clock, so the template does it with the date it already has |
## Interactivity
| Item | Verdict | Justification |
|---|---|---|
| Server-processed forms | **Arc 3** | The Interaction primitive, with ADR-0003's boundary. Fields, validation, action target — plus rate limiting and spam handling, which is why they are one arc and not one feature |
| Webmention replies / reactions | **Arc 3** | The canonical first Interaction; `roadmap.md` already sequences it |
| Client JS for demos only | **Already the position** | The reference theme ships no assets and no JS: icons are Unicode (ADR-0063), highlighting is server-side (ADR-0075), the TOC and the contents list are server-rendered, tabs and reveal panels are `details` with no script |
## IndieWeb
| Item | Verdict | Justification |
|---|---|---|
| Webmention send/receive, Micropub, POSSE, mf2 | **Arc 3 / 4** | `roadmap.md` puts Micropub first on purpose: it buys an existing editor ecosystem and needs no UI, which may remove the admin panel entirely |
| ActivityPub | **TRUNK — already recorded as one** | `ideas/exploration.md` lists it among known trunks. Your "only if it sparks joy" and the sovereignty test agree. Your instinct that it needs its own actor/inbox/signing mechanism rather than the generic webhook path is correct and is why it is a trunk |
| Gemtext | **LEAF** | An output format, and `ideas/exploration.md` stars Gemini as high fit-to-effort for this project's values |
## Admin
| Item | Verdict | Justification |
|---|---|---|
| Dev server | **BUILT** | `-dev on` reveals drafts and future-dated bundles and polls 4×/s; content *and* templates swap atomically as one snapshot, so a template typo keeps the last working theme instead of taking the site down (ADR-0055, ADR-0056, ADR-0077) |
| Live reload (browser auto-refresh) | **Tension** | Needs JS or SSE on every page, against your own antifeature. The rebuild already happens in one poll; a manual refresh costs nothing. Recommend dropping it |
| Git as source of truth | **BUILT by design** | The site root is its own repository, passed with `-site`; nothing in the engine repo is content (ADR-0011) |
| Link / frontmatter checks | **BUILT** | `khosra check`: engine workarounds, broken internal links, missing titles, missing alt text, mixed series ordering, retired shortcode calls. Non-zero exit, and `verify.sh` runs it over the demo site |
## Security & Ops
| Item | Verdict | Justification |
|---|---|---|
| Rate limiting on write endpoints | **Arc 3** | Arrives with the first write endpoint, not before — there is nothing to limit on a read-only server |
| Spam filtering on webmentions | **Arc 3** | With receive. Your honeypot-and-rate-limit stance beats CAPTCHA and is already in the antifeatures |
| Backups | **Operator, not engine** | ADR-0010's sovereignty line. The content is a git repo; the derivative cache is disposable by construction (content-hash names) |
## Analytics
| Item | Verdict | Justification |
|---|---|---|
| Self-hosted / simple counting | **LEAF**, with a cheaper alternative | A counter is a write on the read path — state, IO, and a concurrency question the engine has deliberately avoided. Parsing access logs offline costs zero engine code and answers the same question. Recommend the log route first |
## Antifeatures
Every one is consistent with a decision already recorded, and several are *enforced* rather than intended:
no JS or assets in the reference theme (ADR-0063), server-side highlighting (ADR-0075), server-side TOC
(ADR-0065), no trackers therefore no cookie banner, server-side over client-side as the standing preference.
**Recommendation: record the list as an ADR.** An antifeature that is not written down does not bind a
future decision, and this list is exactly the kind of thing an agent will erode one reasonable-looking
request at a time. It is also the cheapest ADR in the backlog — a decision you have already made.
One internal tension: **"no hover-preview popups (the gwern.net thing)"** versus Extras' **"hover-preview
footnotes/sidenotes"**. Adjacent but arguably different — a footnote is your own text, a link preview is
someone else's page. Worth resolving explicitly in the ADR, and note `:::aside` already gives you
server-rendered margin notes with no JS at all.
---
## Extras
| Item | Verdict | Justification |
|---|---|---|
| Now / Uses / Colophon pages | **BUILT** | Ordinary bundles under `pages/`. The demo site already has `pages/colophon` |
| Guestbook | **Arc 3** | Forms or webmention; piggybacking on that infrastructure is right |
| Blogroll, OPML export | **THEME** + tiny **LEAF** | The list is content; OPML is one more output format |
| Link blog | **BUILT** as convention | A section plus a `type`. Nothing to build |
| TIL | **BUILT** as convention | Same |
| Digital-garden backlinks | **Arc 4** | `roadmap.md` names backlinks and transclusion there. Needs a link graph over the index — a real feature, arc-gated, and transclusion needs a cycle guard |
| Random post | **LEAF**, tiny | One route. Note the discipline: `time.Now()` is confined to `clock.go` and `verify.sh` enforces it by filename; randomness deserves the same containment |
| Yearly wrap-up | **LEAF**, small | A Query by date range plus a listing. The Query primitive is already earned |
| Site changelog | **THEME / content** | A page you write, or `git log` of the content repo rendered — the latter shares the git-reading question with per-post edit history |
| Personal JSON API | **LEAF** | Same feature as "content as data" under SEO |
| Reading / watching / listening log | **BUILT** as convention | A section plus frontmatter |
| Writing stats page | **LEAF**, small | Query plus arithmetic. Pairs with word count |
| Command-palette search (Cmd+K) | **Tension**, and gated | Needs JS on every page, against the antifeature — and needs Search to exist first |
| Sticky / floating TOC | **THEME** | `position: sticky`. The TOC data already exists server-side |
| Hover-preview footnotes / sidenotes | **THEME** | Achievable with CSS alone, and `:::aside` already does margin notes. See the antifeature tension above |
| Copy link / share button | **THEME** | A plain link needs no JS; the clipboard does |
| 404 with personality | **THEME** | The 404 path exists and is tested; the words are the theme's |
| `humans.txt` / `security.txt` | **LEAF** | The raw-passthrough route. Same one feature |
| IndexNow-style ping on publish | **LEAF** after the Effect runner | Outbound |
| PGP / identity proof page | **LEAF** | The raw-passthrough route again |
| EXIF display on photo posts | **LEAF — cheaper than it looks** | The engine *already* opens and inspects every picture for `width`/`height`, memoised and LRU-bounded (ADR-0042, ADR-0044, `memo.go`). EXIF extends an existing pass rather than adding one |
---
## Content-specific
### Tech writing
| Item | Verdict | Justification |
|---|---|---|
| Copy-to-clipboard on code blocks | **Tension** | Needs JS. ~10 lines, but it is JS on ordinary pages, which your antifeatures rule out. Your call, not mine to soften |
| Line highlighting in code blocks | **BUILT** | `hl=3,7-9`, in the numbering the reader sees, plus `title=`, `numbers=`, `start=`, `file=`, `lines=` (ADR-0075) |
| "View as Markdown" / raw source per post | **LEAF**, small | The pattern exists: extras already serve `?raw`. Extending it to a bundle body is a route, not a redesign |
| Corrections / updates section per post | **THEME today** | A frontmatter list plus a fragment. No engine code. And the right call for credibility — I would build this before most of the list |
| Per-post edit history | **LEAF / ?** | Means the engine reads git, which it does not do today. Either an Effect writes a history file, or this stays the operator's. Shares the question with git sync |
| Inline diagrams as a figure template | **BUILT** | `::figure{src alt caption}` is exactly image + caption + alt |
| Jargon / acronym tooltips | **BUILT for one document**, gap across documents | `*[TERM]: definition` expands every whole-word use and renders `<abbr>` (ADR-0062) — tooltip is the browser's `title`. But a definition applies to *that document only*, and `content-model.md` names sharing a fragment between bundles as deliberately not possible yet: it needs somewhere to keep shared parts, which is a disk-contract decision. That gap is the real feature behind both this and the worldbuilding glossary |
| Sandboxed runnable demos | **THEME** + the `styles`/`scripts` lift | An iframe is markup. Isolation is the iframe's, not the engine's |
### Comics
| Item | Verdict | Justification |
|---|---|---|
| Numbered index, newest first, numbering independent of date | **BUILT** | `order` is position and never appears in a URL (ADR-0016); listings sort by date, sequences ascend by `order`. The two are already separate |
| One per page, next / prev | **BUILT** | Sequence navigation on any nested bundle, resolving through language fallback |
| — random | **LEAF**, tiny | One route |
| — grid when more than one per page | **THEME** | Layout |
| Panels, some hidden until revealed | **BUILT** | `:::details` — an expandable panel with no JavaScript (ADR-0064) |
| Description and alt text as separate fields | **THEME today** | Both live in `Extra`; the template decides which goes to `alt` and which to prose. `khosra check` already fails on missing alt text |
| Serialized arcs alongside one-offs | **Agreed open — and a content question** | You call it open and I think you are right to. Worth noting *why* it is cheap to defer: membership is structural (ADR-0033), so an arc is a nested directory and a one-off is not. The archive's reading order is a theme decision on top of data that already exists. Decide it with real content in front of you |
### Artwork
| Item | Verdict | Justification |
|---|---|---|
| Gallery / grid view | **BUILT** + **THEME** | `::gallery` gathers; grid is CSS |
| Lightbox | **THEME** | An `<a>` to the original is the no-JS version and also gives you "view full resolution" |
| Per-piece metadata (medium, tools, dimensions) | **BUILT** | The `Extra` bag, by design (ADR-0002) |
| Before / after slider | **THEME** | CSS-only versions exist; the draggable one is JS. Useful beyond art, as you say |
| "View full resolution" link | **BUILT-able, one line** | The original is already `src` and the derivatives are the `srcset` — the full-res file is already addressable |
| Attribution / license per piece | **THEME** | `Extra` plus a fragment |
| Alt text as a real description, required for this type | **BUILT** (checked) + **?** (per-type requirement) | `khosra check` already fails on missing alt text everywhere. Making it *required for art specifically* needs declared types (queue 7) |
### Creative writing
| Item | Verdict | Justification |
|---|---|---|
| Per-piece template variants — letter, terminal log, clipping | **LEAF — and the most significant item in the list** | This is the **View layer**: `view:` in frontmatter choosing a presentation, `architecture.md`'s named primitive, counter at 0 of 2. Your Cross-cutting bullet restates it, which makes it the second concrete use — the thing that *earns* the primitive. **Arc 2 closes when the View layer lands, and that is where the core stops growing** (`roadmap.md`). You independently asked for the exact feature the roadmap was waiting for, and your framing — one content model, variants layered on top, not a content type per variant — matches the ADR-0023 contract already frozen for it |
| Story status: ongoing / complete / hiatus | **THEME** | `Extra` plus the series index template |
| Worldbuilding glossary with inline definitions | **BUILT for one document**, same cross-document gap as jargon tooltips | See above — the shared-fragment decision is the real blocker for both |
| Content warnings | **THEME today**, **?** for first-class | A frontmatter field works now. "First-class across all four types" means enforced, which needs declared types (queue 7) |
| Epigraphs / dedications | **THEME** | `Extra` plus a fragment |
### Cross-cutting
| Item | Verdict | Justification |
|---|---|---|
| Per-piece template variant across all types | **= the View layer above** | One feature, stated twice. Counts as its second concrete use |
| Alt text and content warnings first-class across types | **?** | "First-class" = enforced per type = declared content types, queue 7, whose trigger is currently "a dated bundle needing to stay out of a feed". Your requirement is a *better* trigger for it than the recorded one |
---
## What this adds up to
**Already shipping, or shipping with a theme fragment you write:** the majority. Notably the whole
admonitions/tabs/panels family, extras folders, includes, series, tags, feeds, sitemap, canonical and
redirects, responsive images, code highlighting with line ranges, abbreviations, TOC, dev server, checks,
and — verified this session — user-defined shortcodes with no rebuild.
**Three items are disproportionately valuable:**
1. **Per-piece template variants** — earns the View layer and closes Arc 2. Architecturally the most
important thing in the list, and you asked for it twice without naming it.
2. **Raw passthrough routes** — one small feature, four list entries, and a real gap today.
3. **Corrections/updates per post** — no engine code at all, and the one item here that changes whether
the writing can be trusted.
**Four collide with recorded decisions** and are surfaced rather than settled: task lists (ADR-0058),
private/secret-link (ADR-0024), git sync (`content-model.md`), external fetching for oEmbed and static
copies (same). One reframe removes the fourth entirely.
**Five need JS on ordinary pages**, against your own antifeature: copy-to-clipboard, Cmd+K, draggable
before/after, lightbox, live reload. Each has a no-JS equivalent except the clipboard.
**The shortest line is the biggest job.** "Search" is one word and is arc-sized, because Bengali
tokenisation is the novel problem this project has been pointing at from the start.