review 130 feature ideas, and delete the lists that held them
One idea at a time: definition, necessity, priority, layer, recommendation. Every row is now shipped, scheduled, parked with a trigger, or dropped with a reason — so both tracking files are gone rather than left as a parallel backlog. Roughly a third already shipped. A quarter needed only a theme fragment or CSS, including several the list assumed were features: arbitrary `theme.*` frontmatter attributes already reach templates through Extra (verified), native lazy loading already ships and is deliberately absent on lead figures, and in-page timelines are a theme-defined container. roadmap.md gains an "Order of work" — nine bodies of work in sequence, because the sections this review added had accumulated without one. Item 0 is a decision, not work: core sits at ~2965 of 3000 and logging alone wants the remainder, so discover.go moves out *and* the ceiling rises with an ADR. feed.go and web/extras.go cannot move; they are resolver cases, and three parked items now wait on that same seam decision. Two ADRs came out of it. ADR-0083: the dependency rule described a practice it forbade. "Stdlib first, always" and "usually 40 lines of stdlib" would, read literally, have argued against all five modules here — every one is a format or algorithm somebody else specified, and their allowlist comments say so. Surfaced when this agent proposed hand-rolling Reed–Solomon for QR encoding. The test is now whether you can verify it, not whether you can write it, with compute-versus-hold as the counterweight. conventions.md and allowed-deps.txt contradicted it and were realigned. ADR-0084: the Effect primitive said every Effect is "idempotent and re-runnable from scratch" — true of derivatives, false of anything outbound, since a sent message cannot be recalled. Split into artifact-producing and outbound, the latter idempotent only against a delivery ledger, at-least-once with a dedupe key, and ledgers in a -state directory distinct from the disposable cache. Asked for as forward-looking design so webmentions need no redesign; recorded as a shape rather than built, the way extensions.md records the Extension struct. Also fixed a STATUS claiming Effects were "not buildable yet" when the derivative pass has inhabited them since ADR-0042. Findings that were defects rather than ideas: the Atom feed emits no <author>, which RFC 4287 requires. HTML comments in content are published verbatim — verified — so anything commented out is already public, and a check warning is scheduled rather than the engine deleting authored bytes. `Page` has no Date, so a theme cannot mark up dt-published or show an article's own date. A frontmatter naming rule after this agent proposed `archive` alongside the accepted `archived`: no near-homograph keys, and name the benefit rather than the vendor. reference/microformats-and-indieweb.md is new and is the reference asked for — microformats2 properties and IndieWeb rel values mapped to where a theme puts them, with spec URLs, marked scheduled. rel="me" is the highest-value lowest-cost item in that space and needs no endpoint at all. Four things this agent got wrong and the human caught: asserting "the engine never fetches" from a sentence scoped to the content repo, twice; inventing a data-sovereignty argument for an item he had filed as "a cool geeky thing", when the raw Markdown in his git already is the sovereignty; the archive naming; and claiming microformats could be fully implemented today. 15 files, +556/-385. No code changed. Nothing was scheduled that this agent could not name a consumer for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -213,7 +213,8 @@ Swept: the old form you grepped for after a rename, or "n/a"
|
||||
|---|---|---|
|
||||
| Interface with one implementation | Anticipating, not designing | Concrete type; wait for the second |
|
||||
| A parameter no caller passes | Speculative flexibility | Delete it |
|
||||
| Reaching for a dependency | Usually 40 lines of stdlib | Write the 40 lines, or stop and ask |
|
||||
| Reaching for a dependency for something small | Usually 40 lines of stdlib | Write the 40 lines, or stop and ask |
|
||||
| Hand-rolling something specified elsewhere | A bug farm whose test vectors you must also transcribe | Take the maintained module (ADR-0083), with its ADR |
|
||||
| Wanting a `switch` on post type in the core | A View or Query is the seam | Move it out |
|
||||
| Needing a mock to test | Wrong seam | `fstest.MapFS` or a `testdata` dir |
|
||||
| Renaming "for clarity" mid-feature | Drive-by refactor | Latent list |
|
||||
|
||||
@@ -57,7 +57,10 @@ the test: `harness/architecture.md`.
|
||||
knob, registry. The counters table in `harness/state.md` holds every threshold and is the only place
|
||||
they are written down: read them, increment them, never anticipate them.
|
||||
2. **No new dependency** without an ADR and human approval. Allowlist:
|
||||
`scripts/allowed-deps.txt`. Stdlib first, always.
|
||||
`scripts/allowed-deps.txt`. Stdlib first for anything small enough to verify by reading it; a maintained
|
||||
module for anything somebody else specified — a format, a grammar, an error-correcting code. The test is
|
||||
*can I verify it*, not *can I write it* (ADR-0083). A module that computes is walkaway-safe; one that
|
||||
holds data is not, whatever its size.
|
||||
3. **Surgical diffs.** Only the lines the feature needs. No renames, no reformatting beyond
|
||||
`gofmt`, no "while I was in there". Spotted something bad? Latent list in `harness/state.md`.
|
||||
4. **The untrusted boundary is absolute.** Anything not from the site root (comments, webmentions,
|
||||
|
||||
+7
-1
@@ -160,7 +160,13 @@ load-bearing mechanism here, and the one with least machine enforcement, which i
|
||||
fails any `.go` change that does not touch `state.md`. That does not prove the counters are *right*;
|
||||
it makes forgetting them impossible, which is the real failure mode.
|
||||
|
||||
**The dependency allowlist** names modules that are permitted, not required. The gate also fails an
|
||||
**The dependency allowlist** names modules that are permitted, not required. **Stdlib first means what you
|
||||
can verify by reading, not everything you could technically write** (ADR-0083): a format, a grammar or an
|
||||
error-correcting code that somebody else specified gets a maintained module, because the work there is
|
||||
verification rather than authorship and a subtly wrong implementation fails silently. The counterweight is
|
||||
unchanged — a module that *computes* is walkaway-safe since its output is reproducible, a module that
|
||||
*holds* data is not, and that is the line rather than size. Every module here is the first case, which is
|
||||
why the rule was amended to describe the practice rather than the practice bent to the rule. The gate also fails an
|
||||
untidy `go.mod`, because `go get` marks a module indirect until something imports it — and an indirect
|
||||
entry is not checked against the allowlist, so an unapproved dependency could sit there unnoticed. `#` starts a comment anywhere on a line, including after an entry — the gate strips those, so an entry may carry the reason it exists. Being listed is permission;
|
||||
`DEPS_MAX` counts what `go.mod` actually pulls in. `scripts/allowed-deps.txt` is the list.
|
||||
|
||||
+1
-1
@@ -44,7 +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 |
|
||||
| **which list owns a piece of pending work** | four kinds, deliberately not merged: **might be built** → `ideas/exploration.md` — the catalogue, plus parked and dropped items each with a trigger or a reason · **is scheduled** → `roadmap.md`, whose "Order of work" says which is next · **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 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 -- <the path it names>` 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) |
|
||||
|
||||
+20
-6
@@ -50,18 +50,32 @@ Derived artifacts and outbound calls: image derivatives, search index, sitemap a
|
||||
webmention sending, POSSE, outbound-link archiving, EPUB builds, future-dated publication.
|
||||
|
||||
Three triggers, and one Effect may accept more than one: **on change** (a bundle changed), **on
|
||||
schedule** (a clock), **on demand** (a CLI subcommand). Every Effect is idempotent and re-runnable
|
||||
from scratch, never writes into the site root — that is the author's data (ADR-0011) — logs and
|
||||
retries on failure rather than dying, and leaves the engine correct if it has never run at all: a
|
||||
missing derivative serves the original, a missing index disables search (invariant 8).
|
||||
schedule** (a clock), **on demand** (a CLI subcommand, or an Interaction needing outbound work such as
|
||||
verifying a received webmention). Every Effect never writes into the site root — that is the author's data
|
||||
(ADR-0011) — logs and retries on failure rather than dying, and leaves the engine correct if it has never
|
||||
run at all: a missing derivative serves the original, a missing index disables search (invariant 8).
|
||||
|
||||
**Two kinds, and only one is re-runnable from scratch (ADR-0084).** An *artifact-producing* Effect writes
|
||||
bytes — derivatives, a search index, an EPUB — and is idempotent by regeneration; losing its output costs
|
||||
time. An *outbound* Effect makes a call somebody else observes — a notification, a webmention, an archive
|
||||
submission — and **cannot** be re-run from scratch, because a sent message cannot be recalled. It is
|
||||
idempotent only against a **delivery ledger** of what already succeeded, and its contract is at-least-once
|
||||
with a stable dedupe key rather than exactly-once, which no network boundary allows.
|
||||
|
||||
Ledgers live in a **state directory** (`-state`), never in the disposable derivative cache (`-cache`):
|
||||
deleting the cache must remain safe, and conflating the two would let a `rm -rf` on regenerable files arm a
|
||||
storm of duplicate calls. Where losing a ledger is survivable, the Effect seeds silently on first run, so
|
||||
loss costs missed calls rather than duplicates.
|
||||
|
||||
**Not an Effect:** anything computable at render time from the page plus the clock. An "this article is
|
||||
old" banner is a **Stage** reading `date` — right on every request, no job, no staleness, nothing
|
||||
written. Inbound reactions are **Interactions**. If an Effect would need to mutate content to be
|
||||
visible, it is a Stage wearing a disguise.
|
||||
|
||||
**STATUS: not buildable yet.** Earned at the first derived artifact; check the Effects counter in
|
||||
`state.md`. Scheduling is an in-process ticker inside the single binary — no cron container and no
|
||||
**STATUS: partly live.** Earned at the first derived artifact and inhabited by one — the derivative pass
|
||||
(ADR-0042), called from `cmd` inside `rebuilder`. The **runner** with real trigger wiring is not built:
|
||||
check the Effects counter in `state.md`, which stands at 1 and extracts at 2. No outbound Effect exists
|
||||
yet, so the ledger and state directory ADR-0084 describes are a recorded shape rather than code. Scheduling is an in-process ticker inside the single binary — no cron container and no
|
||||
queue until ADR-0010's second-service test is actually met.
|
||||
|
||||
**Routing** = URL → (Bundle, View), via a resolver. **STATUS: live.** The resolver was earned at the
|
||||
|
||||
+15
-3
@@ -4,15 +4,17 @@ The style floor. Do not ask about anything here — read it and comply. Disagree
|
||||
goes through an ADR, not a diff.
|
||||
|
||||
## Language and dependencies
|
||||
- Go, current stable release. Stdlib first, every time.
|
||||
- Go, current stable release. Stdlib first, with the balance `CLAUDE.md` rule 2 states (ADR-0083):
|
||||
what you can verify by reading it, not everything you could technically write.
|
||||
- `net/http`, `html/template`, `log/slog`, `os`, `io/fs`, `embed`. Routing is `http.ServeMux` with Go
|
||||
1.22+ patterns — `GET /{section}/{slug}/` and `{path...}` for multi-segment slugs — so no router
|
||||
library is needed. No web framework, ORM, or config library — flags plus environment variables, parsed in one place. The site root
|
||||
(`-site`, `KHOSRA_SITE`) is the only required setting; default templates are `embed`ded so a bare
|
||||
site root renders (ADR-0011).
|
||||
- New dependency = ADR + human approval + `scripts/allowed-deps.txt`. `verify.sh` enforces it.
|
||||
- Prefer 40 lines of obvious code over a dependency doing it in one call — unless the 40 lines would be
|
||||
subtly wrong in cases the author cannot predict, which is why YAML is a dependency (ADR-0020).
|
||||
- Prefer 40 lines of obvious code over a dependency doing it in one call — unless those lines would be
|
||||
subtly wrong in cases the author cannot predict (why YAML is a dependency, ADR-0020), or the task is one
|
||||
somebody else specified, where the work is verification rather than authorship (ADR-0083).
|
||||
|
||||
## Package layout
|
||||
```
|
||||
@@ -43,6 +45,11 @@ wants splitting.
|
||||
because a forgotten expiry serves staleness silently.
|
||||
- Comments explain *why*, never *what*. Delete a comment narrating the next line. One stating a
|
||||
non-obvious invariant is worth ten describing control flow.
|
||||
- **No two frontmatter keys may be near-homographs.** `archive` and `archived` differ by one letter and
|
||||
would have meant "submit this page's outbound links" and "this content is stale" — proposed in the same
|
||||
review and caught only by the human. A key is a word an author types from memory months later, so
|
||||
distinctness matters more than brevity. Name the benefit, never the vendor: `preserve_links`, not
|
||||
`wayback`.
|
||||
- **Shell scripts end in `.sh`, with exactly one exception: `scripts/hooks/pre-commit`.** git locates a
|
||||
hook by exact filename, so a `.sh` suffix makes it invisible and the gate stops running on every commit
|
||||
while still appearing to exist — verified by renaming it in a throwaway repository and watching a commit
|
||||
@@ -74,6 +81,11 @@ sentence, and a gate whose cheapest satisfaction is noise buys noise.
|
||||
Those are the comments worth writing.
|
||||
|
||||
## Errors
|
||||
- **Log levels mean these things, and `Error` is not "look at this".** `Error`: the engine could not do what
|
||||
was asked — a request or build step failed, startup is aborting. `Warn`: it worked around something wrong
|
||||
in content or config and still served, which is all of ADR-0029's category. `Info`: lifecycle and one line
|
||||
per request. `Debug`: off by default, per-request detail while diagnosing. A log line addresses an
|
||||
operator, not its author; if it cannot be acted on, it is `Debug` or it is nothing.
|
||||
- Wrap with `%w` at package boundaries, with operation and path: `parse %s: %w`.
|
||||
- Never log and return the same error. Handle it, or return it.
|
||||
- Request-time render failure degrades: log, serve what exists, never 500 on a missing field.
|
||||
|
||||
@@ -1441,3 +1441,62 @@ also considered for a move under `docs/` and deferred with it, since it is a liv
|
||||
nobody has made.
|
||||
Revisit if: `docs/` is still empty when the first person other than its author tries to use this engine, at
|
||||
which point the absence has stopped being honest and become neglect.
|
||||
|
||||
## ADR-0083 — The dependency test is whether you can verify it, not whether you can write it
|
||||
Date: 2026-08-02 · Status: accepted (amends rule 2's "stdlib first, always")
|
||||
Decision: stdlib first stands for anything small enough to verify by reading it. For a task **somebody else
|
||||
specified** — a wire format, a grammar, an error-correcting code, a Unicode algorithm, a compression scheme
|
||||
— take a maintained module. Everything else about rule 2 is unchanged: an ADR, human approval, an allowlist
|
||||
line with the reason, and `DEPS_MAX`.
|
||||
Why: the rule as written — "stdlib first, always", and the skill's "usually 40 lines of stdlib" — would,
|
||||
read literally, have argued against every dependency this project has taken. All five are the
|
||||
nontrivial-and-specified-elsewhere case, and their own allowlist comments say so: "no stdlib resizer
|
||||
exists", "the only mature pure-Go one". The text lagged the practice, and the gap surfaced when this agent
|
||||
proposed hand-rolling QR encoding: Reed–Solomon over GF(256) plus mask evaluation, roughly 400 lines whose
|
||||
correctness is measured against spec test vectors it would also have to transcribe. Writing such a thing is
|
||||
not the work — verifying it is, and a maintained module has already done both. A hand-rolled encoder that
|
||||
is subtly wrong fails silently, which is this engine's recurring failure mode in a new costume.
|
||||
The counterweight is unchanged and still decides the other direction: a dependency is supply-chain surface
|
||||
and a thing to be walked away from. The sovereignty test applies as always — *if this vanishes, do I lose
|
||||
data or only convenience?* A module that merely **computes** is walkaway-safe, because its output is
|
||||
reproducible by any replacement; a module that **holds** data or state is not, and no amount of
|
||||
nontriviality buys one of those a place here.
|
||||
Consequence: the balance is stated where the rule lives rather than being folded into each ADR. Rule 2 in
|
||||
`CLAUDE.md` and the skill's failure-mode table both gain the second half. Nothing already allowlisted
|
||||
changes, and nothing new is admitted by this decision alone — a module still needs its own ADR. Six modules
|
||||
of a permitted nine are in use, so the first few applications of this need no budget change.
|
||||
Revisit if: a "specified elsewhere" module turns out to hold state or data rather than compute, which is
|
||||
the case this decision does not cover and must not be read as permitting.
|
||||
|
||||
## ADR-0084 — Effects come in two kinds, and only one is re-runnable from scratch
|
||||
Date: 2026-08-02 · Status: accepted (amends the Effect primitive in `architecture.md`)
|
||||
Decision: an Effect is **artifact-producing** or **outbound**, and the difference is contractual rather than
|
||||
descriptive.
|
||||
An *artifact-producing* Effect writes bytes somewhere the engine may write — image derivatives, a search
|
||||
index, an EPUB. It is idempotent and re-runnable from scratch, because regenerating yields the same result;
|
||||
losing its output costs time and nothing else.
|
||||
An *outbound* Effect makes a call somebody else observes — a notification, a webmention, an archive
|
||||
submission. It is **not** re-runnable from scratch: re-running it duplicates messages that cannot be
|
||||
recalled. It is idempotent only *with respect to a delivery ledger* recording what has already succeeded,
|
||||
and its contract is **at-least-once with a stable dedupe key** rather than exactly-once, which is
|
||||
unachievable across a network boundary.
|
||||
Ledgers and any other state that is neither the author's content nor regenerable live in a **state
|
||||
directory** (`-state`), distinct from the disposable derivative cache (`-cache`). Deleting the cache must
|
||||
stay a safe act; deleting state must not be one, and conflating them would make a `rm -rf` on a regenerable
|
||||
directory arm a notification storm. Where a ledger's absence is survivable, the Effect seeds silently on
|
||||
first run — recording current state and calling nothing — so loss costs missed calls rather than duplicates.
|
||||
Why: the primitive said "every Effect is idempotent and re-runnable from scratch", written when the only
|
||||
Effect was the derivative pass. The first outbound Effect breaks that sentence rather than extending it, and
|
||||
discovering the contradiction while building would have meant either redesigning the primitive under
|
||||
pressure or quietly violating it. Recording the split now costs a paragraph; the alternative was a redesign.
|
||||
Consequence: the retry shape is stated once for everything outbound — bounded in-process backoff, then a
|
||||
ledger of successes, with `rebuilder`'s startup pass as the long-term backstop, and no persistent queue.
|
||||
Three outbound Effects are foreseen (notify, webmention sending, outbound-link archiving) and the last two
|
||||
share an input, the set of a page's external links, so that extraction wants to be separable from the first
|
||||
one built. The shared dispatcher is **not** built until the second consumer exists — rule 1 is unchanged by
|
||||
this decision, which records a shape rather than authorising an abstraction.
|
||||
An Interaction may trigger an outbound Effect: verifying a received webmention means fetching a stranger's
|
||||
URL, which is outbound work that must not happen on the request path. That is the existing "on demand"
|
||||
trigger, not a fourth one.
|
||||
Revisit if: an Effect is neither artifact-producing nor outbound, which would mean a third kind and a real
|
||||
gap in this split.
|
||||
|
||||
@@ -14,6 +14,75 @@ arc gets built early "since we are in the file anyway."
|
||||
**Done when:** a URL reliably maps to a bundle, in two languages, with URLs you are willing to promise
|
||||
are permanent.
|
||||
|
||||
## Order of work — decided 2026-08-02
|
||||
|
||||
Eight bodies of work came out of the idea review. Arcs say what *earns* a primitive; this says what happens
|
||||
next, and it exists because the sections below accumulated without a sequence.
|
||||
|
||||
0. **Core budget: do both.** Decided 2026-08-02. Move `internal/web/discover.go` into a feature (~71 lines,
|
||||
once ADR-0081's seam passes the live index) **and** raise `CORE_LOC_MAX` with an ADR stating what moved
|
||||
and why, per `HARNESS.md`'s rule for any ceiling change. `feed.go` and `web/extras.go` **cannot** move:
|
||||
they are resolver cases, not exact paths, which is a separate seam decision.
|
||||
1. **Logging.** First build, and first for a reason beyond its own value: notify's retries and backoff are
|
||||
unobservable without it, and there is no access log at all today.
|
||||
2. **Relative links in content.** Authoring QoL, and it makes links survive `slug:` renames.
|
||||
3. **Events → notify.** Outbound Effect per ADR-0084.
|
||||
4. **Summary and composed metadata**, including author identity — one addition wanted by feeds, SEO, the
|
||||
OpenGraph image and IndieWeb identity.
|
||||
5. **IndieWeb markup and identity.** Mostly theme work; needs `Page.Date` and 3 first.
|
||||
6. **Arc 2: the View cluster** — View layer, declared content types, minimal settings cascade. Closes Arc 2
|
||||
and freezes the core, which is why 0 matters.
|
||||
7. **The Effect runner and outbound-link archiving.** Second outbound Effect, earns the extraction.
|
||||
8. **Exact-form search.** No core growth, no Effect, so it can move earlier if wanted.
|
||||
|
||||
Unarced items land in the gaps, in any order.
|
||||
|
||||
## Logging
|
||||
|
||||
**No access log exists** and slog is never configured — no `slog.New`, no level, no JSON — despite
|
||||
`conventions.md` making the import mandatory. So this is a gap, not polish.
|
||||
|
||||
Scope, deliberately small and adding **no new package**, because `log/slog` *is* the module and a wrapper
|
||||
around it would be an abstraction with one caller:
|
||||
|
||||
- `-log-level` and `-log-format` wired once in `cmd`. JSON is the part that makes logs parseable.
|
||||
- One middleware around the mux: method, path, status, duration, bytes. This *is* the access log, and it is
|
||||
what makes offline log analysis a real answer to analytics rather than an assumed proxy.
|
||||
- The level convention in `conventions.md`, so code written later lands at the right level.
|
||||
- **Backfill and re-level existing code.** Today: 38 Error, 7 Warn, 4 Info, 0 Debug. Most of those Errors are
|
||||
ADR-0029's "logged, not fatal" cases where the engine coped — a misspelled directive, an asset path
|
||||
climbing out of its bundle — which are warnings. Error meaning "something failed" is what lets an operator
|
||||
triage at all. The same pass adds lines where the engine does something notable and says nothing: rebuild
|
||||
completion, derivative counts, a language fallback serving a different variant.
|
||||
|
||||
## Relative links in content
|
||||
|
||||
An author writes `./post2` or `../notes-on-water/` relative to the **file's** place on disk, and the engine
|
||||
emits the served URL — so a link works in an editor preview and on the site. Output stays root-relative,
|
||||
which is already the rule (ADR-0039).
|
||||
|
||||
**It also makes links survive renames.** A hand-written `/posts/a-better-name/` breaks when a `slug:`
|
||||
changes; a relative link resolves through key → route, and a slug never moves the key (ADR-0035). Legitimate
|
||||
because it changes an address between two representations of one target, not an author's words — the
|
||||
distinction ADR-0045 actually draws.
|
||||
|
||||
The traps, all of them from disk layout not matching URL layout:
|
||||
|
||||
- A directory bundle sits one level deeper than a single-file one, so relative depth differs between them.
|
||||
Inherent to the bundle model, not to be papered over.
|
||||
- Resolve to the **route**, never the key, or a slugged bundle gets the wrong address.
|
||||
- Prefer the current language and fall back, as `Lookup` already does.
|
||||
- **Assets already work by accident** — `cover.jpg` resolves because the URL shape mirrors the disk shape —
|
||||
so rewrite only destinations that resolve to a bundle and leave absolute, root-relative, fragment-only,
|
||||
`mailto:` and non-bundle files alone.
|
||||
- Refuse escapes above the site root, the rule `::include` and code `file=` already enforce.
|
||||
- Accept `./post2` and `./post2.en.md` alike: the bare form is friendlier, the full filename is what makes an
|
||||
editor preview resolve.
|
||||
|
||||
Parse-phase, so it lives in a feature with **no core growth**; the one seam it needs is a key → route
|
||||
resolver callback set at wiring time, exactly like the existing `sections` and `compose` seams. Ships with an
|
||||
eats-prose table and a `khosra check` rule, because a mistyped relative path is silent today.
|
||||
|
||||
## Arc 2 — earn the primitives at first real use
|
||||
In this order, each at its trigger, never before:
|
||||
1. **Query** — at the first collection page (section index, latest posts).
|
||||
@@ -37,6 +106,150 @@ IndieWeb adapters (Micropub first — it buys an existing editor ecosystem for f
|
||||
only if still wanted, then delight: backlinks, transclusion, page-as-program, revision-as-craft.
|
||||
Optional admin panel here, as a second client over the ContentAPI.
|
||||
|
||||
## IndieWeb markup and identity
|
||||
|
||||
**Scheduled, and almost entirely theme work.** Marking pages up as `h-entry`, `h-card` and `h-feed` makes
|
||||
them parseable by anything in the IndieWeb ecosystem; the `rel` values make them *actionable*. Properties,
|
||||
placement and spec URLs: `reference/microformats-and-indieweb.md`.
|
||||
|
||||
`rel="me"` is the highest-value, lowest-cost item in the whole space — a few profile URLs in `<head>`, and an
|
||||
identity claim becomes checkable rather than asserted. It is what Mastodon's verified link reads and what
|
||||
IndieAuth identifies you by, and it needs no endpoint and no protocol.
|
||||
|
||||
The engine's share is three fields, each scheduled elsewhere and each wanted by other features too:
|
||||
`Page.Date` (Unarced — `dt-published` cannot be expressed without it), `.Summary` (the summary work), and
|
||||
author identity in site config (the metadata work). Nothing here needs a new mechanism.
|
||||
|
||||
Deliberately **not** emitted yet: `rel="webmention"` and `rel="micropub"` advertise endpoints, so publishing
|
||||
them before the endpoints exist would advertise a lie. `rel="hub"` is dropped outright.
|
||||
|
||||
## Unarced — small features, ordered by appetite
|
||||
|
||||
Things that earn no primitive, move no counter and block nothing. They have no arc because they have no
|
||||
sequence: any of them can land in the gaps of whichever arc is open. A row belongs here only if all three
|
||||
hold — anything that moves a counter or gates another feature is arc work and goes above, and anything
|
||||
whose shape is undecided is an idea and goes to `ideas/`.
|
||||
|
||||
Kept short on purpose. A long list here means small work is being used to avoid the arc.
|
||||
|
||||
| Item | Why it is small | Note |
|
||||
|---|---|---|
|
||||
| `Page.Date` in the theme contract | one field plus a contract line | A defect rather than a feature: `date` is lifted out of `Extra`, so a bundle template cannot read its own publication date and the reference theme shows none. Feeds and listings have it; the article does not |
|
||||
| Reading time and word count | a walk over text nodes already in hand | Parse-phase, so it moves no transform counter — counting `Page.HTML` instead would be worse engineering adopted to move a number. Excludes code blocks, and the words-per-minute divisor is **per language**, in the chrome table beside the month names. Must include content pulled in by `::include`: merged fragments come free since they are one tree, but `include: embed` converts separately and needs its count accumulated through the parse context, the way shortcode asset names already are |
|
||||
| Inline image alignment | an argument the fragment already receives | Not merely possible but depended on by live content, so the demo must show it and the reference stylesheet must carry the classes. Hand-written `<img class=…>` works too, since the site root is trusted |
|
||||
| `khosra check` warns on HTML comments in content | one rule | **Comments are published today** — verified: `<!-- x -->` reaches the served HTML verbatim, block and inline, because raw HTML renders (ADR-0060) and a comment is raw HTML. The docs never said so, so an author assuming comments are private is publishing them. The warning catches it on existing content without the engine deleting authored bytes; `[//]: # (…)` is the form that renders nothing, and belongs in `content-model.md` as the recommended private note |
|
||||
| Atom `<author>` | one element | **A defect**: RFC 4287 requires `atom:feed` to carry an author unless every entry does, and `feed.go` emits none — so the feed is invalid and readers show no author. Blocked only on author identity existing in site config |
|
||||
| `archived:` documented | a table row | Accepted, unlifted, read from `Extra` by the theme. The author's override of the derived staleness signal |
|
||||
|
||||
## Events, and the outbound dispatch they share
|
||||
|
||||
**The first feature to build after the reviews.** The engine emits events; something else decides what they
|
||||
mean. n8n already runs and does routing, formatting and per-platform posting, which is precisely why the
|
||||
engine's share of this is small — it ships facts to one endpoint and owns none of the fan-out.
|
||||
|
||||
Three events, detected by diffing snapshots: `published` (became visible — new, undrafted, or a future date
|
||||
passed), `updated` (content hash changed while staying visible), `unpublished` (became invisible). All three
|
||||
are always sent; **the consumer filters, the engine does not**. Arc 3 adds `webmention.received` to the same
|
||||
channel, which is why this is an event type rather than a publish hook.
|
||||
|
||||
**Fingerprint by content hash, never mtime.** A git clone rewrites mtimes, so an mtime fingerprint reports
|
||||
every bundle as `updated` after every deploy. Hashing is the established pattern (ADR-0042).
|
||||
|
||||
This is an **outbound** Effect, so its delivery contract, ledger, state directory and silent-seeding rule
|
||||
are ADR-0084's, not restated here. Its dedupe key is bundle key + event type + content hash. Backoff of
|
||||
roughly 1s, 5s, 30s, 2m, 10m before giving up and logging loudly; a goroutine is legitimate because
|
||||
`conventions.md` bars them from the *render path* and an Effect is off it by definition.
|
||||
|
||||
The endpoint URL lives in an **environment variable**, not `site.yaml`: it carries a token and `site.yaml` is
|
||||
in the content git repo. `notify: false` skips a bundle — **stage-toggle #2**, which with `preserve_links`
|
||||
earns the settings cascade.
|
||||
|
||||
**What this unlocks, and the extraction to expect.** Outbound webmention and archive.org submission are the
|
||||
same shape — for each thing derived from a published bundle, POST somewhere, retry, remember success — and
|
||||
the last two share an input, the set of outbound links in a page. Build notify **concretely** first; the
|
||||
second consumer is what earns the shared dispatcher, and the link extraction wants to be separable from the
|
||||
start so it can feed both without being generalised early.
|
||||
|
||||
## The Effect that earns the runner
|
||||
|
||||
Outbound-link archiving is the **second Effect**, and the extraction is the point. `architecture.md` already
|
||||
names it as an Effect example and `ideas/exploration.md` stars it, so this is a written intention that was
|
||||
never built rather than a new idea. The counter stands at 1, due at 2, so building it forces the Effect
|
||||
runner and its trigger wiring — change, schedule, demand — out of `cmd` where the derivative pass currently
|
||||
sits alone.
|
||||
|
||||
Four parked items wait on that runner and none of them on archive.org: corrections derived from repository
|
||||
history, IndexNow pings, a search index, and outbound syndication. That is the reason to sequence this
|
||||
rather than its own value, which is real but modest.
|
||||
|
||||
Per page, `preserve_links: false` switches it off, read by presence so absence keeps the default — and **it
|
||||
is stage-toggle #1** for the settings cascade (`ideas/deferred-decisions.md`), which was designed to carry
|
||||
exactly these. One does not earn the cascade; the second toggle does, instead of a third bespoke boolean.
|
||||
Not `noarchive`, which is the robots meta value meaning "keep no copy of this page" — nearly the opposite
|
||||
concern. And not `archive`, which is a near-homograph of the accepted `archived` staleness label
|
||||
(`conventions.md`).
|
||||
|
||||
## Exact-form search
|
||||
|
||||
Server-rendered, no JavaScript, one page at `/search`. **Exact-form** is the honest name: whitespace
|
||||
tokenising plus the NFC that is already unconditional (ADR-0015) finds the word you typed, in English or
|
||||
Bengali, if it appears in that form. Bengali morphology — বই / বইয়ের / বইগুলো are one lemma — is *not*
|
||||
solved by it and is documented as absent, so nobody mistakes this for the research in `ideas/`.
|
||||
|
||||
**No index, and no Effect.** The whole corpus is already resident: `Site.byKeyLang` holds every `Bundle`
|
||||
including its `Body`, swapped atomically on rebuild. So v1 scans what is already in memory and ranks in one
|
||||
pass; an inverted index is an optimisation, and building it first would be a cache with no measurement
|
||||
behind it — the reasoning that declined to memoise sequence resolution. If scanning ever feels slow, the
|
||||
index arrives *with* a number, and only then is there a question about where it lives: memory, the cache
|
||||
directory like derivatives, or lazy on first query.
|
||||
|
||||
**No core growth.** `/search` is an exact path, so it fits ADR-0081's seam as it stands, and the feature
|
||||
lives entirely under `internal/ext/`. Nothing here competes with the View cluster for the remaining core
|
||||
budget, and nothing here waits on the Effect runner.
|
||||
|
||||
Ranking is arithmetic, so it is written rather than imported — ADR-0083's test, answered in the stdlib
|
||||
direction for once.
|
||||
|
||||
## Summary, and composed metadata
|
||||
|
||||
**`summary` is promised and unread.** `content-model.md` says "explicit summary; otherwise derived" and
|
||||
nothing reads it in either form — the seventh documented-but-absent claim this review has found. Atom
|
||||
entries carry no summary either, so feeds gain from this as much as any metadata tag does, which is the
|
||||
better argument since feeds are the chosen notification layer.
|
||||
|
||||
Resolution order, explicit first:
|
||||
|
||||
1. `summary` in frontmatter.
|
||||
2. Everything before the first line consisting solely of `===` **preceded by a blank line**. The blank line
|
||||
is load-bearing, not style: a Setext underline must immediately follow its paragraph, so `Summary\n===`
|
||||
becomes an `<h1>` while `Summary\n\n===` stays a paragraph. Both verified. Without the precondition the
|
||||
engine could not tell the delimiter from an author's genuine Setext heading, and would silently take
|
||||
everything before it.
|
||||
3. The first 20–40 **words** — words rather than characters, which sidesteps runes and grapheme clusters
|
||||
entirely, since a count that split a Bengali matra from its consonant would produce broken text. Prefer
|
||||
ending at a sentence boundary inside that window; cut at the upper bound otherwise.
|
||||
|
||||
**Metadata is composed, not authored twice.** A `metadata:` parent key in frontmatter carries `description`
|
||||
and friends; the final set resolves from page metadata, then taxonomy, then site-wide defaults declared in
|
||||
`site.yaml`. That is site → bundle resolution, so with `preserve_links` it is **cascade consumer #2** — the
|
||||
second concrete use that earns the settings cascade rather than a third bespoke mechanism
|
||||
(`ideas/deferred-decisions.md`).
|
||||
|
||||
**OpenGraph preview image**, in order: the page's first *large* image, then an image named in frontmatter,
|
||||
then a site default. The hard half is already built — every image is inspected for dimensions, memoised and
|
||||
LRU-bounded (ADR-0042, ADR-0044) — so this needs one decision: what "large" means as a threshold.
|
||||
|
||||
**Author identity is part of this, and four things want it.** `site.yaml` declares `base` and `title` and
|
||||
nothing about a person. An `h-card`, `rel="me"`, `rel="author"`, the Atom `<author>` element and a default
|
||||
OpenGraph image all resolve from the same handful of fields — name, URL, photo, and a list of profile URLs
|
||||
for `rel="me"`. One addition, four consumers, so it lands with the site-wide metadata block rather than
|
||||
separately. Markup guidance for all of it: `reference/microformats-and-indieweb.md`.
|
||||
|
||||
**Arbitrary theme attributes need nothing.** Verified: `theme: {hero_image: hero.jpg}` in frontmatter is
|
||||
readable as `{{.Extra.theme.hero_image}}` today, because unlifted keys land in `Extra` and nesting survives
|
||||
(ADR-0002). The work is *documenting* the convention so themes can rely on it — the same gap that cost this
|
||||
project's own author the underscore rule.
|
||||
|
||||
## Cross-cutting — when actually needed
|
||||
Cache (when requests feel slow) — in-process first; Redis only once a second process must share it,
|
||||
and never as the only copy of anything (ADR-0010). Containerisation: one Dockerfile, one binary, no
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ 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**
|
||||
- [bengali-search-morphology.md](bengali-search-morphology.md) — why exact-form search ships without stemming, and what solving it actually requires. **parked**
|
||||
- [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
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Bengali morphology for search
|
||||
|
||||
Status: parked — original work, not a sub-task
|
||||
Raised: 2026-08-02
|
||||
|
||||
Exact-form search is scheduled without this and **documents its own absence**. This file exists so that
|
||||
"add search" can never be read as having solved it.
|
||||
|
||||
## What exact-form search already does
|
||||
|
||||
Whitespace tokenising plus the NFC normalisation that is already unconditional (ADR-0015) finds the word you
|
||||
typed, in English or Bengali, if it appears in that form. That is genuinely useful and it is not
|
||||
English-only — Bengali separates words with spaces, so this is **not** the CJK segmentation problem.
|
||||
|
||||
## What is actually missing
|
||||
|
||||
**Morphology.** বই, বইয়ের, বইগুলো are one lemma carrying case and plural suffixes, and exact-form matching
|
||||
treats them as three unrelated terms. A reader searching for the base form finds none of the inflected uses.
|
||||
English gets away with naive matching far better than Bengali does, which is why this is not a polish item.
|
||||
|
||||
Concretely absent:
|
||||
|
||||
- **No mature Go stemmer exists.** Snowball has no Bengali algorithm, so there is nothing to import — this
|
||||
is the case ADR-0083's "take the module" rule cannot help with, because there is no module.
|
||||
- **Suffix stripping needs a real rule set**, not a hand-written list guessed at from a few examples. Getting
|
||||
it wrong produces confidently wrong results, which is worse than exact-form matching that is honest about
|
||||
what it does.
|
||||
- **Transliteration search** — typing "boi" to find বই — is a separate feature again, and arguably wanted
|
||||
more than stemming by anyone typing on a phone keyboard.
|
||||
- **Orthographic variation** beyond NFC: the same word written with different but valid conjunct forms.
|
||||
|
||||
## Why it is treated as research
|
||||
|
||||
`harness/roadmap.md` singles this out: *"Bengali tokenisation for search is the genuinely novel problem here
|
||||
— solve it well and deliberately, not as a side effect of adding search."* `ideas/exploration.md` repeats it
|
||||
as the one item nobody else will solve for you.
|
||||
|
||||
The failure mode being avoided is specific: a stemmer bolted on during a search feature, tuned until the
|
||||
author's own test queries pass, then quietly wrong for every word he did not think to try.
|
||||
|
||||
## Trigger
|
||||
|
||||
Deciding to do the linguistic work — deliberately, as its own piece, with a way to evaluate results beyond
|
||||
"my queries look right". Until then exact-form search ships and says what it does not do.
|
||||
+50
-5
@@ -18,14 +18,59 @@ Use `/leaf <topic>` to get a verdict written into this file.
|
||||
|
||||
## Verdicts
|
||||
|
||||
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.
|
||||
The human's 130-item feature list was reviewed one item at a time on 2026-08-02 and the file holding it is
|
||||
gone: everything is now either shipped, scheduled in `harness/roadmap.md`, parked below, or dropped below.
|
||||
Roughly a third already shipped, a quarter needed only a theme fragment or CSS, and nine bodies of work were
|
||||
scheduled. Two ADRs came out of it — ADR-0083 (dependencies) and ADR-0084 (Effects).
|
||||
|
||||
| Body of work | Verdict | Reduces to | Note |
|
||||
|---|---|---|---|
|
||||
| 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 |
|
||||
| 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 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-post edit history is parked in that same file |
|
||||
| Per-piece template variants | the highest-value item in the list | **View** | Scheduled, not parked: the second concrete use that earns the View primitive, and `roadmap.md` closes Arc 2 when it lands |
|
||||
| Bengali morphology for search | original work, not a sub-task | — | [bengali-search-morphology.md](bengali-search-morphology.md). Exact-form search is scheduled without it and documents its own absence |
|
||||
|
||||
## Parked, with triggers
|
||||
|
||||
Each of these was reviewed and deliberately not scheduled. The trigger is what would change that — a
|
||||
condition, not a mood.
|
||||
|
||||
| Item | Trigger | What was decided |
|
||||
|---|---|---|
|
||||
| Date-grouped listings | wanting any one of: a cross-page timeline, a yearly wrap-up, a date-grouped archive | One partition beside the existing section one serves all three, following ADR-0046's rule that the engine partitions and the theme decides whether it *looks* grouped. Individually none justifies the work |
|
||||
| Inline code highlighting | writing enough technical prose to feel the lack | Syntax settled in advance: a trailing attribute block — `` `code`{lang=python} `` — never a marker inside the span, since a code span is literal and this engine's recurring defect is new syntax eating prose. Matches Pandoc/Djot and the existing `## Title {#id}` form. Wants its own ADR, because authored syntax is a contract with the author's files |
|
||||
| Ruby annotations for glosses | the free path failing | Works today via raw HTML. The zero-cost path first: a theme rendering existing `*[TERM]:` abbreviations as `<ruby>`, since a gloss above versus on hover is presentation over data already collected. Caveat: abbreviations expand at *every* occurrence. Standing note — the inline delimiter budget is nearly spent (`~ ^ = *` all taken), which argues against inventing marks |
|
||||
| Archive.org snapshot links beside external links | something wanting a pass over authored anchors more than this does | Needs a parse-phase walk over link nodes; a theme cannot do it. Modest value. Distinct from *submitting* links, which is scheduled |
|
||||
| JSON Feed | a reader or client you actually use asking for it | Atom covers every consumer. `/feed.json` site-wide is an exact path and could be a feature cheaply; section and tag scopes are resolver cases, so decide it with that seam |
|
||||
| Content as data / a personal JSON API | wanting it for its own sake | The human's own framing: "a cool geeky thing", may never be used. The sovereignty argument for it is **wrong** — the raw Markdown in his git already is the sovereignty. `/all.json` site-wide is an exact-path escape; per-bundle is resolver-gated |
|
||||
| oEmbed | building the capture feature below | Reframed: not a render-time embed, which ADR-0080 refuses, but *capture-time metadata discovery* — how a snapshot learns a URL's title, author and thumbnail |
|
||||
| Static copies of external content | wanting to quote something that may vanish | Engine-produced, not by another tool. Shape decided: a `khosra snapshot` subcommand following `scaffold`'s precedent for writing to the site root, into **the bundle** rather than the disposable cache since a snapshot must outlive its source, storing **sanitised** text, image and attribution — never the remote HTML, which would execute, because site-root content is trusted by location (ADR-0060) |
|
||||
| Image processing, as one cluster | any one of them mattering | Cropping (a parameter on the derivative pass, folded into the content hash), EXIF display (the engine already inspects every image for dimensions, so this extends a shipped pass), a view-full-resolution link (nearly free — the original is already `src`), before/after comparison (CSS-only variants exist; the draggable one is JS and refused), and the AVIF gap (no Go decoder, so it passes through unresized). Clustered at the human's request rather than scattered |
|
||||
| POSSE back-links | wanting a page to link its own copies | Syndication itself is absorbed by the scheduled notify work — n8n posts to platforms. The residual is `u-syndication`: recording where copies live, which needs somewhere to put the URLs |
|
||||
| Micropub | wanting to publish from a phone | The endpoint is easy; **IndieAuth is the bulk**. Persistence is not a blocker and the resolution is recorded: the engine writes through `os.Root` (scaffold's precedent) and the operator's existing pull/commit picks it up, so the engine still never touches git |
|
||||
| "View as Markdown" / raw source per post | the comment problem being solved | `?raw` on the existing bundle URL avoids inventing a resolver path and reuses the convention extras already use. **Blocked on a leak**: HTML comments in content are published verbatim today (verified), so serving raw source would expose anything commented out. The scheduled `check` warning is the prerequisite |
|
||||
| IndexNow ping on publish | caring about Bing and Yandex | An outbound Effect, nearly free once the dispatcher exists. Honest limit: Google ignores IndexNow, so it is cheap and marginal rather than cheap and valuable |
|
||||
| Blogroll and OPML export | wanting others to subscribe to your whole list | The blogroll is content and free today. OPML is one marshaller at an exact path, so a feature with no core growth |
|
||||
| Random post | wanting a discovery affordance | One exact-path route, architecturally free, and honestly a toy. Note: randomness deserves the containment `time.Now()` already gets — `verify.sh` confines the clock to `clock.go` because ambient nondeterminism hides staleness |
|
||||
|
||||
## Dropped, with reasons
|
||||
|
||||
Kept because a one-line reason is the cheapest defence against relitigating a decision in four months.
|
||||
|
||||
| Item | Why |
|
||||
|---|---|
|
||||
| EPUB generation | Calibre, Pandoc and percollate already convert web to EPUB. The engine's real contribution is *structure* — sequences already publish reading order — so the revisit trigger is "external tools produce a bad book", not "want EPUB" |
|
||||
| Gemtext output | Readership is approximately zero, and its only real value was proving a View can target something other than HTML. Not worth carrying for that |
|
||||
| Static export | Cannot serve Arc 3's write endpoints or in-memory search, so it is a second permanently crippled product rather than a hedge. The hosting insurance it appears to buy already exists: Markdown in git, one portable binary |
|
||||
| Incremental rebuilds | A whole page renders in ~63µs. This solves a build measured in minutes, a scale this site will notice instantly if it ever reaches |
|
||||
| Live reload | Buys not pressing refresh, at the price of JS or SSE on every page — and the rebuild already lands within one poll |
|
||||
| Self-hosted analytics counting | Write-on-read means state and IO on the hot path. Access logs contain every fact a counter would, cost zero engine code, and leak nothing. `goaccess` answers it in an afternoon |
|
||||
| Incoming rebuild webhook | Polling already covers it, so this spends a write endpoint, the untrusted boundary and rate limiting to save two seconds |
|
||||
| Outgoing chat webhooks as a general system | Absorbed: the scheduled notify Effect emits events and n8n owns routing, formatting and per-platform posting. A configurable per-target rule engine in the engine would serve one caller forever |
|
||||
| Twitter cards, meta description | Theme work once `.Summary` exists; Twitter reads the OpenGraph already emitted |
|
||||
| Design tokens, print stylesheet, columnar layout, in-page timeline | All CSS or a theme-defined container, in a file the engine does not own (ADR-0023) |
|
||||
| Copy-to-clipboard on code blocks, Cmd+K palette | JS on ordinary pages, refused by ADR-0080. The palette also needs search first, and the search page is one keystroke from the address bar |
|
||||
| WebSub (`rel="hub"`) | Push instead of polling for feeds, at a scale this site does not have |
|
||||
| Writing stats page | "Just for fun" by the human's own framing, the same standard that dropped readability scoring |
|
||||
|
||||
## Catalog
|
||||
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
# 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.
|
||||
@@ -21,3 +21,4 @@ Out of agent context by default, this index included. Opened only when the human
|
||||
- [math-on-the-web.md](math-on-the-web.md) — rendering maths with no JavaScript, and the state of Go TeX→MathML
|
||||
- [syntax-highlighting-choices.md](syntax-highlighting-choices.md) — chroma's cost, what it gives, and why there is no lighter option
|
||||
- [goldmark-behaviours.md](goldmark-behaviours.md) — the goldmark surprises that caused or nearly caused defects
|
||||
- [microformats-and-indieweb.md](microformats-and-indieweb.md) — microformats2 properties and IndieWeb `rel` values, mapped to where a theme puts them and what the engine supplies.
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# microformats2 and IndieWeb markup — a theme author's reference
|
||||
|
||||
Established 2026-08-02 from the microformats2 and IndieWeb specifications, checked against what this engine
|
||||
supplies today. Vocabulary is somebody else's standard, so this file records it rather than deciding it;
|
||||
what the *engine* promises a theme is `harness/theme-contract.md`.
|
||||
|
||||
**Status: actively scheduled.** The markup, the `rel` values and the config that feeds them are on the
|
||||
roadmap under "IndieWeb markup and identity" — not parked. Almost all of it is theme work; the engine's share
|
||||
is `Page.Date`, `.Summary` and author identity in site config, each scheduled separately and each wanted by
|
||||
other features too.
|
||||
|
||||
## Where the specs live
|
||||
|
||||
Canonical spec homes, recorded from knowledge rather than fetched — stable URLs, but confirm against the page
|
||||
if a detail matters.
|
||||
|
||||
| Topic | URL |
|
||||
|---|---|
|
||||
| microformats2, the umbrella | `https://microformats.org/wiki/microformats2` |
|
||||
| parsing rules (what a consumer actually does) | `https://microformats.org/wiki/microformats2-parsing` |
|
||||
| `h-entry` | `https://microformats.org/wiki/h-entry` |
|
||||
| `h-card` | `https://microformats.org/wiki/h-card` |
|
||||
| `h-feed` | `https://microformats.org/wiki/h-feed` |
|
||||
| `h-cite` | `https://microformats.org/wiki/h-cite` |
|
||||
| `rel="me"` | `https://microformats.org/wiki/rel-me` and `https://indieweb.org/rel-me` |
|
||||
| `u-syndication`, for POSSE back-links | `https://indieweb.org/u-syndication` |
|
||||
| POSSE, the practice | `https://indieweb.org/POSSE` |
|
||||
| Webmention (W3C REC) | `https://www.w3.org/TR/webmention/` |
|
||||
| Micropub (W3C REC) | `https://www.w3.org/TR/micropub/` |
|
||||
| IndieAuth | `https://indieauth.spec.indieweb.org/` |
|
||||
| WebSub (W3C REC) | `https://www.w3.org/TR/websub/` |
|
||||
| Atom, which requires `<author>` | `https://www.rfc-editor.org/rfc/rfc4287` |
|
||||
| the IndieWeb wiki generally | `https://indieweb.org/` |
|
||||
|
||||
The parsing spec is the one worth reading before arguing about a property: it decides whether a class reads
|
||||
an attribute or text, which is where most mistakes come from.
|
||||
|
||||
## The four prefixes
|
||||
|
||||
| Prefix | Means | Read from |
|
||||
|---|---|---|
|
||||
| `p-` | plain text | the element's text content |
|
||||
| `u-` | a URL | `href`, `src`, or text |
|
||||
| `dt-` | a datetime | `datetime` attribute preferred, text otherwise |
|
||||
| `e-` | an element tree | the element's inner HTML, markup included |
|
||||
|
||||
The prefix is the whole trick: `p-name` and `e-content` differ only in whether markup survives.
|
||||
|
||||
## h-entry — one post
|
||||
|
||||
Goes on the `<article>` a bundle page already renders.
|
||||
|
||||
| Property | Where it goes | Engine field |
|
||||
|---|---|---|
|
||||
| `p-name` | the `<h1>` | `.Title` |
|
||||
| `p-summary` | a summary paragraph | `.Summary` — scheduled |
|
||||
| `e-content` | the wrapper around the body | `.HTML` |
|
||||
| `dt-published` | `<time datetime="…">` | **blocked — see below** |
|
||||
| `dt-updated` | `<time datetime="…">` | **blocked — see below** |
|
||||
| `u-url` | a link to the permalink | `.Canonical` |
|
||||
| `u-uid` | same element as `u-url`, both classes | `.Canonical` |
|
||||
| `p-category` | each tag link | `.Tags` |
|
||||
| `p-author` | a nested `h-card`, or a `u-url p-author` link to one | site config — scheduled |
|
||||
| `u-featured` | the lead image | the OG-image work, scheduled |
|
||||
|
||||
Machine-readable form goes in the attribute and the human form in the text — which is already the theme
|
||||
contract's standing rule for dates, so `dt-published` needs no new discipline, only the field.
|
||||
|
||||
## h-card — a person
|
||||
|
||||
Goes on an author page (`pages/about` is the natural home) and, in miniature, wherever a post names its
|
||||
author.
|
||||
|
||||
| Property | Typical element |
|
||||
|---|---|
|
||||
| `p-name` | the name |
|
||||
| `u-url` | a link to the site or the page itself |
|
||||
| `u-photo` | an avatar `<img>` |
|
||||
| `u-uid` | the canonical identity URL, usually the site root |
|
||||
| `p-note` | a one-line bio |
|
||||
|
||||
`rel="author"` on a post should point at whatever page carries the full `h-card`.
|
||||
|
||||
## h-feed — a stream
|
||||
|
||||
Goes on a listing: the section index, the root listing, a tag listing.
|
||||
|
||||
| Property | Where |
|
||||
|---|---|
|
||||
| `p-name` | the listing's heading |
|
||||
| `p-author` | an `h-card`, usually once per feed rather than per entry |
|
||||
| children | each entry marked `h-entry` with at least `p-name` and `u-url` |
|
||||
|
||||
## h-cite — a quoted or replied-to post
|
||||
|
||||
Only relevant once responses exist. `p-name`, `u-url`, `p-author`, `dt-published` describing *somebody
|
||||
else's* post, nested inside your `h-entry` under one of:
|
||||
|
||||
`u-in-reply-to` · `u-like-of` · `u-repost-of` · `u-bookmark-of`
|
||||
|
||||
## Link relations — the other half, and the half that is not microformats
|
||||
|
||||
These are `rel` values in `<head>` or in body links. They are what makes an IndieWeb consumer able to *do*
|
||||
anything with a page.
|
||||
|
||||
| rel | Purpose | State here |
|
||||
|---|---|---|
|
||||
| `canonical` | the permalink | **emitted** |
|
||||
| `alternate` (+ `type`) | feeds, and language variants via `hreflang` | **emitted** |
|
||||
| `me` | identity links — a profile URL that links back verifies you own it. This is what Mastodon's verified-link checkmark reads, and what IndieAuth uses to identify you | scheduled |
|
||||
| `author` | points at the page carrying the `h-card` | scheduled |
|
||||
| `webmention` | your receiving endpoint | endpoint does not exist |
|
||||
| `micropub` | your publishing endpoint | endpoint does not exist |
|
||||
| `authorization_endpoint`, `token_endpoint` | IndieAuth | not built |
|
||||
| `hub` | WebSub, for push rather than polling | **dropped** — push instead of polling matters at a scale this site does not have |
|
||||
|
||||
`rel="me"` is the highest-value, lowest-cost item in this table: a list of profile URLs in `<head>`, and it
|
||||
is what makes an identity claim checkable rather than asserted.
|
||||
|
||||
## What this space needs that is *not* markup
|
||||
|
||||
Found while writing this, and each is recorded where it belongs rather than here:
|
||||
|
||||
- **Author identity in site config.** `site.yaml` declares `base` and `title` and nothing about a person. An
|
||||
`h-card`, `rel="me"`, `rel="author"`, the Atom `<author>` element and a default OpenGraph image all want
|
||||
the same handful of fields. One addition, four consumers.
|
||||
- **The Atom feed emits no `<author>`.** RFC 4287 requires `atom:feed` to carry one unless every entry does,
|
||||
so the feed is invalid as it stands and readers show no author. A defect, not a feature.
|
||||
- **`.well-known/` is already servable.** IndieAuth metadata and anything else the specs put there works
|
||||
today through the passthrough feature (ADR-0081), including `.tmpl` interpolation of the site's own base —
|
||||
so endpoint discovery documents cost nothing when the endpoints eventually exist.
|
||||
|
||||
## The one thing a theme cannot express today
|
||||
|
||||
`dt-published` and `dt-updated` need the bundle's date, and **`Page` has no `Date` field**. It is lifted out
|
||||
of `Extra` by the parser, so a bundle template cannot reach it at all; feeds and listings have it, an article
|
||||
does not. Already on the Unarced list as a defect. Everything else above can be marked up now.
|
||||
@@ -1,5 +1,6 @@
|
||||
# Non-stdlib dependency allowlist. One module path per line; # starts a comment.
|
||||
# Adding a line requires an ADR in harness/decisions.md. Stdlib first, always.
|
||||
# Adding a line requires an ADR in harness/decisions.md. Stdlib first for what you can verify by
|
||||
# reading it; a maintained module for anything somebody else specified (ADR-0083).
|
||||
# Only direct requirements are checked here; the total module count is capped by DEPS_MAX.
|
||||
# Infrastructure clients (Redis, S3, search) are dependencies like any other and get no exemption.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user