enable task lists, superseding the decision that excluded them

ADR-0058 kept task lists out as "a note-taking affordance, not a publishing
one". That reasoning measured the wrong axis: a checklist inside a published
technical piece — setup steps, a runbook, a what-I-tried list — is publishing,
and nothing else in the dialect expresses "this item is done" without the author
hand-writing an entity. ADR-0078 supersedes that half and records why.

The half of ADR-0058 that mattered is untouched: extension.GFM stays refused,
because the bundle drags linkify in with the tables it is wanted for, and
linkify rewrites an author's plain text into markup — the line ADR-0034 draws.
One named extension is not a bundle, and wire.go now says so where the
temptation to reach for GFM will next appear.

ADR-0058's Status line names its successor, so a reader arriving there learns
the task-list sentence no longer holds. Same in-place Status annotation the
mutability rule allows, Decision text untouched.

Checkboxes render disabled: static markup, nothing clickable, nothing stored. A
reader with scripting off sees the same page, which is the property the whole
theme is built on. goldmark adds no class to the list, so theme.css finds it
with :has rather than the engine inventing markup to be styled by.

Demo carries the case ADR-0051 requires — a colophon checklist of what this
build does and does not do, including the unticked "ship a single byte of
JavaScript", which is true of that page and asserted by the test's absent list.

No counter moves: state.md's counters are explicit that an upstream extension
enabled in the list is dialect, not a feature of this engine — only a package
under internal/ext/ counts.

8 files, +55/-10. 1 line of engine code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 19:00:51 +06:00
co-authored by Claude Opus 5
parent 4fff6fcd43
commit 69f43e5791
8 changed files with 55 additions and 10 deletions
+3
View File
@@ -132,6 +132,9 @@ var exampleFeatures = []featureCase{
absent: []string{"<script", `style="color:`}},
{what: "grouped panels are tabs, and need no script", path: "/pages/colophon/", code: 200,
expect: []string{`<details name="run" open>`, "<summary>Serve your own</summary>"}, absent: []string{"<script"}},
{what: "a task list renders disabled checkboxes and nothing interactive", path: "/pages/colophon/", code: 200,
expect: []string{`<input checked="" disabled="" type="checkbox"`, `<input disabled="" type="checkbox"`},
absent: []string{"<script"}},
{what: "a lone panel is an expando", path: "/pages/about/", code: 200,
expect: []string{"<details>", "<summary>What is deliberately not shown</summary>"}},
{what: "an admonition renders inside a comic chapter too", path: "/comics/the-long-monsoon/the-flood/", code: 200,
+3
View File
@@ -35,6 +35,9 @@ func extenders(partial render.Partial) []goldmark.Extender {
extension.Table,
extension.NewFootnote(extension.WithFootnoteIDPrefixFunction(shortcodes.FootnotePrefix)),
extension.DefinitionList,
// TaskList alone, never extension.GFM: the bundle would drag linkify in with it, and linkify
// rewrites an author's plain text into markup, which is the line ADR-0034 draws (ADR-0078).
extension.TaskList,
notation.New(),
shortcodes.New(partial),
}
+6 -4
View File
@@ -298,6 +298,7 @@ them is a render transform, and the list lives in `cmd/khosra/wire.go` where fea
| Highlight | `==marked==` |
| Abbreviations | `*[HTML]: HyperText Markup Language` on its own line; every whole-word use expands |
| Heading ids | automatic from the heading's text, or declared: `## Title {#stable-anchor}` |
| Task lists | `- [ ]` and `- [x]` in a list item, rendered as a **disabled** checkbox — static markup, nothing to click and nothing stored (ADR-0078) |
Declare an id when the anchor must outlive the wording: a derived id changes with the text, so rewording a
heading breaks every link to it (ADR-0066). Two headings with the same words get suffixed rather than
@@ -311,10 +312,11 @@ An abbreviation's definition may sit anywhere in the file, before or after the u
nothing itself. It applies to that document only: a term defined in a page does not reach an included
fragment, which is parsed on its own bytes exactly as footnotes are.
Deliberately absent, so their absence is a decision rather than an oversight: **task lists** (a note-taking
affordance, not a publishing one), **linkify** (it rewrites an author's plain text into markup, which is the
line ADR-0034 draws), **CJK line breaking** (wrong script family — it does nothing for Bengali), and the
**GFM bundle**, which would drag the first two in with the tables it is wanted for.
Deliberately absent, so their absence is a decision rather than an oversight: **linkify** (it rewrites an
author's plain text into markup, which is the line ADR-0034 draws), **CJK line breaking** (wrong script
family — it does nothing for Bengali), and the **GFM bundle**, which would drag linkify in with the tables
it is wanted for. Task lists were on this list until ADR-0078 and are now enabled on their own, which is
the distinction: one named extension is not a bundle.
**Footnotes inside an included file** get ids namespaced by that file — `_method-fn:1` rather than `fn:1`
because an include is converted on its own bytes (ADR-0038) and would otherwise number from one all over
+23 -1
View File
@@ -908,7 +908,9 @@ Revisit if: someone wants the reconciliation *moment* recorded rather than the c
different fact and belongs in the arc retro log.
## ADR-0058 — The Markdown dialect is CommonMark plus five, named once
Date: 2026-08-01 · Status: accepted
Date: 2026-08-01 · Status: accepted, except the task-list exclusion, which ADR-0078 supersedes. The refusal
of the GFM bundle and of linkify stands and is the load-bearing half. The Decision text below stands as
written — read the task-list sentence as the position that was held, not the one that holds.
Decision: tables, footnotes, definition lists, strikethrough and automatic heading ids are enabled; task
lists, linkify, CJK line breaking and the GFM bundle are refused. The list lives in `cmd/khosra/wire.go`
beside the features, and `content-model.md` carries the authored form. Footnote ids inside an included file
@@ -1302,3 +1304,23 @@ theme every time rather than only when templates changed, which is microseconds
Expensive — `web.Handler`'s signature changed and twenty test construction sites moved with it, and a
`Renderer` can no longer be handed around and updated, which no caller wanted anyway.
Revisit if: reparsing the theme on every content change ever shows up in a profile.
## ADR-0078 — Task lists are enabled, superseding ADR-0058's exclusion
Date: 2026-08-02 · Status: accepted (supersedes the task-list half of ADR-0058)
Decision: `extension.TaskList` joins the dialect. `- [ ]` and `- [x]` render as goldmark emits them, with a
disabled checkbox — static markup, no script, no interactivity. `extension.GFM` is still refused, and
linkify with it.
Why: ADR-0058 excluded task lists as "a note-taking affordance, not a publishing one". That reasoning was
about the wrong axis. A checklist inside a published technical piece — setup steps, a migration runbook, a
what-I-tried list — is publishing, and the reader benefit is the same whether or not the author also uses
checkboxes for private notes. The human asked for them with that use in mind, and no other mechanism here
expresses "this item is done" without the author hand-writing an entity.
The narrower refusal ADR-0058 also made is untouched and is the part that mattered: the GFM bundle stays
out, because it drags linkify in with the tables it is wanted for, and linkify rewrites an author's plain
text into markup — the line ADR-0034 draws. Enabling one named extension is not enabling a bundle.
Consequence: one line in `cmd/khosra/wire.go` and no counter moves — an upstream extension enabled in the
list is dialect rather than a feature of this engine (`state.md` counters, "does not count" column). The
checkbox is `disabled`, so a reader cannot tick it and nothing is stored; a theme that wants the list to
read as prose unbullets it in CSS, which the reference theme now does.
Revisit if: authors start using task lists for working notes inside published bundles, in which case the
answer is `extras/`, which already renders Markdown and is excluded from every listing — not a change here.
+3 -3
View File
@@ -30,7 +30,7 @@ table owns.
| `internal/ext/scaffold/` | writes one draft directory bundle into a site root through `os.Root`: never an overwrite |
| `internal/ext/watch/` | polls `content/` and `templates/` on an interval it is given, ignores editor droppings, and reports a settled change (ADR-0022, ADR-0048, ADR-0056). `site.yaml` is deliberately not fingerprinted (ADR-0055) |
| `internal/ext/check/` | third feature: validates a site root — what the engine worked around, broken internal links, missing titles and alt text, mixed series ordering, and calls left in the retired shortcode form (ADR-0059) |
| `cmd/khosra/wire.go` | the only list of enabled features (`extensions.md`), the `theme` function that builds the renderer this build ships (ADR-0072), and the Markdown dialect with it — tables, footnotes, definition lists, strikethrough (ADR-0058) |
| `cmd/khosra/wire.go` | the only list of enabled features (`extensions.md`), the `theme` function that builds the renderer this build ships (ADR-0072), and the Markdown dialect with it — tables, footnotes, definition lists, strikethrough, task lists (ADR-0058, ADR-0078) |
| `internal/web/resolve.go` | URL → (key, lang, page, tag, feed, extras) or a canonical redirect |
| `internal/web/extras.go` | the extras route: listing, one entry selected, or `?raw` bytes, all behind the bundle lookup |
| `internal/web/asset.go` | files inside a bundle's own directory, looked up through the owning bundle so visibility can only ever inherit (ADR-0024) |
@@ -47,8 +47,8 @@ key (ADR-0035) — a paginated listing per section, tag listings global and
section-narrowed, sequence navigation and a series archive on any nested bundle, `static/` verbatim, a directory bundle's own files under its
URL, generated derivatives under `/derived/`, Atom feeds per site,
section and tag, a bundle's extras as a browsable tree, plus `/robots.txt` and `/sitemap.xml`.
Markdown is CommonMark plus tables, footnotes, definition lists, heading ids and the inline marks
(`~sub~`, `^sup^`, `==mark==`, `~~strike~~`, `*[TERM]:`), and nothing else (ADR-0058, ADR-0061, ADR-0062); HTML an author writes renders, because the site root is trusted (ADR-0060). Chrome text, dates and digits render in English or Bengali; authored text is untouched but for typographic
Markdown is CommonMark plus tables, footnotes, definition lists, heading ids, task lists and the inline marks
(`~sub~`, `^sup^`, `==mark==`, `~~strike~~`, `*[TERM]:`), and nothing else (ADR-0058, ADR-0061, ADR-0062, ADR-0078); HTML an author writes renders, because the site root is trusted (ADR-0060). Chrome text, dates and digits render in English or Bengali; authored text is untouched but for typographic
smoothing (ADR-0034); line breaking is left to CSS (ADR-0045). This repo holds engine source only — the site root is external and passed with
`khosra check` validates a site root and exits non-zero on anything that makes it wrong; `khosra new`
scaffolds a draft bundle into one. A running server notices changes under `content/` and `templates/` by
+2 -2
View File
@@ -6,9 +6,9 @@ Every top-level declaration in the engine, with its line. Read this before openi
file: it answers "where does X live" and "what is in this package" without the bodies. What each
file is *for* lives in `state.md`; why it is that way lives in `decisions.md`.
## cmd/khosra — 305 lines + 292 test
## cmd/khosra — 308 lines + 295 test
check.go 45 · main.go 177 · new.go 42 · wire.go 41
check.go 45 · main.go 177 · new.go 42 · wire.go 44
- check.go:16 func runCheck(args []string)
- main.go:23 func main()
@@ -27,3 +27,13 @@ without the copies drifting from it — each keeping the line numbers it really
:::tip{title="Nothing is hidden"}
Panels are `<details>`, which every browser opens without help. A reader with scripting off sees the same page.
:::
## What this build does and does not do
A task list is checked off in the source and rendered as a **disabled** checkbox — nothing here is
clickable, and nothing is stored (ADR-0078):
- [x] Highlight code on the server, so no script parses anything in your browser
- [x] Draw icons from Unicode rather than shipping an icon font
- [x] Serve every panel above as plain `<details>`
- [ ] Ship a single byte of JavaScript on a page like this one
+5
View File
@@ -14,6 +14,11 @@ header .site { font-weight: 600; margin: 0 0 0.25rem; }
nav.sections a, nav.languages a, nav.tags a { margin-right: 0.75rem; }
nav.sequence, nav.ends { display: flex; gap: 1rem; justify-content: space-between; margin: 1.5rem 0; }
ul.extras, ol.archive { padding-left: 1.25rem; }
/* A task list is an ordinary <ul> whose items open with a checkbox — goldmark adds no class, so `:has` is
how a theme recognises one without the engine inventing markup for it (ADR-0078). A bullet beside a box
says the same thing twice. */
ul:has(> li > input[type="checkbox"]) { list-style: none; padding-left: 0; }
li > input[type="checkbox"] { margin-right: 0.4em; }
.kind { color: #6b6b6b; font-size: 0.85em; }
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.75rem; }
.gallery figure { margin: 0; }