diff --git a/harness/README.md b/harness/README.md index 38c0908..fde0b52 100644 --- a/harness/README.md +++ b/harness/README.md @@ -44,6 +44,7 @@ that is the finding to report. | language, translation, fallback | ADR-0004 + ADR-0009 | | deploy, containers, external services | ADR-0010 | | whether a future feature is worth building | `ideas/exploration.md` + `roadmap.md` | +| **which list owns a piece of pending work** | four kinds, deliberately not merged: **might be built** → `ideas/exploration.md` (catalog) and `ideas/feature-list-triage.md` (per-item verdicts) · **is scheduled** → `roadmap.md` (arcs) · **is broken and known** → the Latent list in `state.md` · **waits on an observation** → `ideas/deferred-decisions.md` "Triggers — the one list". Merging them would flatten four different levels of authority and make pending work expensive to ignore, which is the property that keeps `ideas/` safe to keep | | what to build next | the harness first (`CLAUDE.md` §1 read order) and `git log` for what landed, **then** `.scratch/continue.md` for the continuation point and `roadmap.md` for the arc. The handoff is temporary and ungated: check anything it calls pending with `git log -- ` before planning from it | | how the harness works — a gate, counter, budget, the loop | `HARNESS.md` + `scripts/verify.sh` + `CLAUDE.md` | | what one commit contains, and when one happens | `conventions.md` "Git" (shape) + `CLAUDE.md` §4 (cadence, ADR-0052) | @@ -69,6 +70,7 @@ here; nothing else may state the value: | The sovereignty test | `roadmap.md` Governors | | Package layout and the forbidden package names | `conventions.md` | | What exists in the code right now | `state.md` inventory | +| What un-defers a deferred decision | the Triggers table in `ideas/deferred-decisions.md` | A value restated in two places is not redundancy for safety; it is a future contradiction waiting for whichever copy gets edited alone. diff --git a/ideas/README.md b/ideas/README.md index 30c637f..5868126 100644 --- a/ideas/README.md +++ b/ideas/README.md @@ -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 diff --git a/ideas/corrections-from-history.md b/ideas/corrections-from-history.md new file mode 100644 index 0000000..f0ad421 --- /dev/null +++ b/ideas/corrections-from-history.md @@ -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. diff --git a/ideas/deferred-decisions.md b/ideas/deferred-decisions.md index 36638bc..f067ab6 100644 --- a/ideas/deferred-decisions.md +++ b/ideas/deferred-decisions.md @@ -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 diff --git a/ideas/exploration.md b/ideas/exploration.md index cbb1d85..635ac5d 100644 --- a/ideas/exploration.md +++ b/ideas/exploration.md @@ -18,9 +18,14 @@ Use `/leaf ` 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 diff --git a/ideas/feature-list-triage.md b/ideas/feature-list-triage.md new file mode 100644 index 0000000..5884060 --- /dev/null +++ b/ideas/feature-list-triage.md @@ -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:` 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 `` (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 `` 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.