correct five claims about behaviour this engine does not have

All five were found by checking content-model.md against the parser rather than
by any gate, which is the point: verify.sh catches a dangling path and a missing
ADR number, never a sentence that is merely untrue.

The Scaffolding section documented `khosra demo <empty dir>` writing a generated
site. ADR-0051 deleted that generator eleven commits ago in favour of the tracked
site in examples/, so the paragraph described a subcommand main.go never
dispatched — and justified itself with "nothing in the engine repository is
content", which the committed demo site contradicts. Deleted rather than
rewritten: what the demo is belongs to state.md's inventory, and restating it
here would have broken the single-source rule to fix a smaller problem.

ADR-0050 is where that claim originated and it still read "Status: accepted",
so a reader arriving there had no way to learn the generator was gone. Its
Status line now names ADR-0051 as superseding that half. This deviates from
decisions.md being append-only, so the exception is recorded in the mutability
column where the convention lives: a Status line may be annotated in place, the
Decision text never.

"The parser does not read `slug` yet" was false and contradicted by the
frontmatter table twelve lines above it in the same file. Deleted.

The Sequences section and the Sequence doc comment both said `draft` is not
honoured "because no bundle carries it yet". Two errors: the demo site carries
one, and draft is honoured — membership resolves through Lookup, which is the
single place ADR-0024 hides unpublished bundles. The doc now says what actually
happens, including that prev/next closes over the gap.

state.md said draft "lands in Extra unread". It is lifted to Bundle.Draft and
deleted from Extra, and content_test.go already asserted it.

Evidence: a three-chapter series with a draft middle, served by a freshly built
binary. The landing lists First Rain and Third Rain; chapter one's next points at
three/, not two/; the draft's own URL is 404. That absence has no test, so it is
now a latent item with the reason it is safe by construction.

6 files, +25/-25. No counter moves. surface.md regenerated: the comment gained a
line and shifted ten declaration numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 16:58:45 +06:00
co-authored by Claude Opus 5
parent 8f5d479f06
commit be92b4b957
6 changed files with 25 additions and 25 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ Read the one you need. Do not read them all.
|---|---|---| |---|---|---|
| `architecture.md` | The six primitives, invariants, per-primitive STATUS | Only via an ADR | | `architecture.md` | The six primitives, invariants, per-primitive STATUS | Only via an ADR |
| `state.md` | What exists now: inventory, earn-it counters, latent items | Every feature | | `state.md` | What exists now: inventory, earn-it counters, latent items | Every feature |
| `decisions.md` | ADR log — one entry per load-bearing choice | Append-only | | `decisions.md` | ADR log — one entry per load-bearing choice | Append-only. One exception: a superseded entry's **Status** line is annotated in place to name what replaced it, because a reader arriving at the older entry has no other way to learn it was replaced. Never the Decision text — that stands as history |
| `roadmap.md` | Arcs, earn-triggers, the core freeze point | When an arc completes | | `roadmap.md` | Arcs, earn-triggers, the core freeze point | When an arc completes |
| `content-model.md` | On-disk layout, frontmatter, post types, permalinks — **what the parser accepts today**, never a plan | When the disk format changes | | `content-model.md` | On-disk layout, frontmatter, post types, permalinks — **what the parser accepts today**, never a plan | When the disk format changes |
| `conventions.md` | Go style floor, package layout, perf and test rules | Rarely; via ADR if contested | | `conventions.md` | Go style floor, package layout, perf and test rules | Rarely; via ADR if contested |
+5 -9
View File
@@ -164,7 +164,7 @@ numbers stay ASCII digits, and a slug is whatever the key says: `/bn/lekha/page/
spelling of it. Localisation is for the words around the content, never the address of it (ADR-0034). A spelling of it. Localisation is for the words around the content, never the address of it (ADR-0034). A
Bengali variant is therefore reached at the same key under a prefix — `/bn/posts/hello-world/`. A `slug` Bengali variant is therefore reached at the same key under a prefix — `/bn/posts/hello-world/`. A `slug`
override renames the bundle in every language at once (ADR-0035); nothing gives one variant an address of override renames the bundle in every language at once (ADR-0035); nothing gives one variant an address of
its own. The parser does not read `slug` yet. its own.
A language prefix wins over a section of the same name, so a site with Bengali content may not also have A language prefix wins over a section of the same name, so a site with Bengali content may not also have
a section called `bn`. The engine treats a leading segment as a language only when some bundle is written a section called `bn`. The engine treats a leading segment as a language only when some bundle is written
@@ -197,17 +197,13 @@ language fallback chain, so a chapter missing in Bengali still appears in Bengal
than breaking prev/next. Reading order is *ascending*, the opposite of a dated listing: a sequence's than breaking prev/next. Reading order is *ascending*, the opposite of a dated listing: a sequence's
"prev" is the earlier entry. "prev" is the earlier entry.
`draft` is not honoured yet, because no bundle carries it and nothing else excludes drafts either; the A draft or future-dated member is not in the sequence at all: membership resolves through the same
feature that adds the field adds it here in the same change. `Lookup` every other path to a bundle uses, and that is where unpublished bundles are hidden (ADR-0024) —
one rule in one place rather than a second check here. So prev/next closes over the gap, and `-dev on`
reveals the member in its proper position without changing anything else.
## Scaffolding ## Scaffolding
`khosra demo <empty dir>` writes a whole site that exercises every feature — both languages with a fallback, a
series with ordered chapters, a gallery, a figure, an include, extras, tags across sections, a slug with an
alias, an undated page, a draft, a template override, `static/` and `site.yaml`. Its prose is generated filler,
not stored content: nothing in the engine repository is content (ADR-0011), and a demo that cannot express a
feature is evidence of a gap in the engine.
`khosra new posts/hello-world` writes `content/posts/hello-world/index.en.md` — a **directory** bundle, because `khosra new posts/hello-world` writes `content/posts/hello-world/index.en.md` — a **directory** bundle, because
only that shape can own local files, so scaffolding the other kind would hand an author a page their pictures only that shape can own local files, so scaffolding the other kind would hand an author a page their pictures
cannot live beside. `-lang` picks the language suffix and `-title` the title, which otherwise comes from the cannot live beside. `-lang` picks the language suffix and `-title` the title, which otherwise comes from the
+3 -1
View File
@@ -734,7 +734,9 @@ Revisit if: a fourth set-once callback appears. Three would say the renderer wan
struct rather than a constructor plus setters. struct rather than a constructor plus setters.
## ADR-0050 — The root lists everything; `khosra demo` writes a site, not content in this repo ## ADR-0050 — The root lists everything; `khosra demo` writes a site, not content in this repo
Date: 2026-07-31 · Status: accepted Date: 2026-07-31 · Status: accepted for the root listing; the `khosra demo` half is superseded by ADR-0051,
which deleted the generator in favour of the tracked site in `examples/`. The Decision text below stands as
written — read it as history, and never as a subcommand that exists.
Decision: `/` serves a listing of every bundle, newest first, paginated like any other — and 404s only when Decision: `/` serves a listing of every bundle, newest first, paginated like any other — and 404s only when
nothing is published. And `khosra demo` writes a site root that exercises every feature, generating its filler nothing is published. And `khosra demo` writes a site root that exercises every feature, generating its filler
rather than copying stored files, into an empty directory the human names. rather than copying stored files, into an empty directory the human names.
+3 -2
View File
@@ -60,8 +60,8 @@ polls four times a second (ADR-0056).
`-site` (ADR-0011). `site.yaml` declares `base` and `title`; with a base, canonical, hreflang and OpenGraph `-site` (ADR-0011). `site.yaml` declares `base` and `title`; with a base, canonical, hreflang and OpenGraph
URLs go absolute (ADR-0039). URLs go absolute (ADR-0039).
Frontmatter the parser lifts today: `title`, `date`, `tags`, `aliases`, `order`, `slug`. `include: embed` is read from `Extra` by the renderer; merging is the default (ADR-0066, ADR-0076). Every other key in Frontmatter the parser lifts today: `title`, `date`, `tags`, `aliases`, `order`, `slug`, `draft`. `include: embed` is read from `Extra` by the renderer; merging is the default (ADR-0066, ADR-0076). Every other key in
`content-model.md`'s table — including `draft` and `type` — lands in `Extra` unread, so that table `content-model.md`'s table — including `type` — lands in `Extra` unread, so that table
is the accepted format, not a list of what runs. is the accepted format, not a list of what runs.
`examples/demo-site/` is a complete site kept in the repository to be read and served — 30 bundles across six `examples/demo-site/` is a complete site kept in the repository to be read and served — 30 bundles across six
@@ -123,6 +123,7 @@ with a stated reason. A list nothing drains is a graveyard of known defects.
| A gallery's images carry no `alt` | `width`/`height` now come from the original (ADR-0042), so only alt text is missing, and a filename does not supply one. An empty `alt` is honest for a picture the page has already introduced | Captions per gallery entry — a sidecar or a frontmatter list — if the reference theme ever needs them | | A gallery's images carry no `alt` | `width`/`height` now come from the original (ADR-0042), so only alt text is missing, and a filename does not supply one. An empty `alt` is honest for a picture the page has already introduced | Captions per gallery entry — a sidecar or a frontmatter list — if the reference theme ever needs them |
| Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a `Lookup` | Measured at the same time as the pictures (ADR-0044): a whole page is ~63µs, so this is not what costs anything. Remembering it would be a cache with no measurement behind it | A page render exceeding a few milliseconds, which is also what would revive the parked cache model | | Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a `Lookup` | Measured at the same time as the pictures (ADR-0044): a whole page is ~63µs, so this is not what costs anything. Remembering it would be a cache with no measurement behind it | A page render exceeding a few milliseconds, which is also what would revive the parked cache model |
| The root listing's `<title>` repeats itself — "A Khosra Demo · A Khosra Demo" | Spotted 2026-08-01 by looking at the served page, not by any test: `base.html` joins page title and site title unconditionally, and at the root those are the same string. Cosmetic, and the fix is one `if` in a template — theme layer, not engine | The next time `base.html` is edited for any reason — its old trigger named queue entry G4, which has been dropped | | The root listing's `<title>` repeats itself — "A Khosra Demo · A Khosra Demo" | Spotted 2026-08-01 by looking at the served page, not by any test: `base.html` joins page title and site title unconditionally, and at the root those are the same string. Cosmetic, and the fix is one `if` in a template — theme layer, not engine | The next time `base.html` is edited for any reason — its old trigger named queue entry G4, which has been dropped |
| A draft member's absence from a sequence has no test | Proven by hand against the real binary on 2026-08-02 — a three-chapter series with a draft middle lists two members, prev/next closes over the gap, and the draft 404s. It holds by construction: `members()` resolves through `Lookup`, which is the one place ADR-0024 hides unpublished bundles, so there is no second code path to drift | The first change to `members()`, or to how `Lookup` decides visibility |
| Under `include: embed`, a fragment's footnotes render where the include sits, so a long one puts an `<hr>` and a numbered list mid-article | Spotted 2026-08-01 by looking at the served page, not by any test. It is ADR-0038's documented consequence, and the ids are correctly namespaced (ADR-0058); only the placement reads badly. Merging is the default since ADR-0076, so this is now something an author opts into by asking for containment | Nothing: it is the documented cost of the model you chose | | Under `include: embed`, a fragment's footnotes render where the include sits, so a long one puts an `<hr>` and a numbered list mid-article | Spotted 2026-08-01 by looking at the served page, not by any test. It is ADR-0038's documented consequence, and the ids are correctly namespaced (ADR-0058); only the placement reads badly. Merging is the default since ADR-0076, so this is now something an author opts into by asking for containment | Nothing: it is the documented cost of the model you chose |
## Open questions ## Open questions
+10 -10
View File
@@ -22,9 +22,9 @@ check.go 45 · main.go 177 · new.go 42 · wire.go 41
- wire.go:18 func theme(siteFS fs.FS, settings content.Settings) (*render.Renderer, error) - wire.go:18 func theme(siteFS fs.FS, settings content.Settings) (*render.Renderer, error)
- wire.go:31 func extenders(partial render.Partial) []goldmark.Extender - wire.go:31 func extenders(partial render.Partial) []goldmark.Extender
## internal/content — 1043 lines + 558 test ## internal/content — 1044 lines + 558 test
clock.go 12 · content.go 451 · doc.go 5 · extras.go 92 · settings.go 59 · site.go 424 clock.go 12 · content.go 451 · doc.go 5 · extras.go 92 · settings.go 59 · site.go 425
- clock.go:9 var now = time.Now - clock.go:9 var now = time.Now
- clock.go:12 func Now() time.Time { return now() } - clock.go:12 func Now() time.Time { return now() }
@@ -86,14 +86,14 @@ clock.go 12 · content.go 451 · doc.go 5 · extras.go 92 · settings.go 59 · s
- site.go:243 func (s *Site) Run(q Query) []Bundle - site.go:243 func (s *Site) Run(q Query) []Bundle
- site.go:268 func (s *Site) keys() []string - site.go:268 func (s *Site) keys() []string
- site.go:284 type Sequence struct - site.go:284 type Sequence struct
- site.go:304 func (s *Site) Sequence(key, lang string) (Sequence, bool) - site.go:305 func (s *Site) Sequence(key, lang string) (Sequence, bool)
- site.go:325 func (s *Site) parent(key string) (string, bool) - site.go:326 func (s *Site) parent(key string) (string, bool)
- site.go:335 func (s *Site) has(key string) bool - site.go:336 func (s *Site) has(key string) bool
- site.go:344 func (s *Site) members(series, lang string) []Bundle - site.go:345 func (s *Site) members(series, lang string) []Bundle
- site.go:369 func (b Bundle) hasTag(slug string) bool - site.go:370 func (b Bundle) hasTag(slug string) bool
- site.go:382 func (b Bundle) Section() string - site.go:383 func (b Bundle) Section() string
- site.go:394 func (s *Site) Everything() []Bundle - site.go:395 func (s *Site) Everything() []Bundle
- site.go:407 func (s *Site) Sections() []string - site.go:408 func (s *Site) Sections() []string
## internal/ext/check — 231 lines + 168 test ## internal/ext/check — 231 lines + 168 test
+3 -2
View File
@@ -299,8 +299,9 @@ type Sequence struct {
// page is about. // page is about.
// //
// Members resolve through the language fallback chain, so a chapter missing in this language still holds // Members resolve through the language fallback chain, so a chapter missing in this language still holds
// its place in the reading order rather than breaking prev/next (ADR-0009). `draft` is not honoured // its place in the reading order rather than breaking prev/next (ADR-0009). Membership goes through Lookup,
// because no bundle carries it yet (content-model.md). // which is where unpublished bundles are hidden (ADR-0024), so a draft member is absent rather than
// filtered here — one rule in one place.
func (s *Site) Sequence(key, lang string) (Sequence, bool) { func (s *Site) Sequence(key, lang string) (Sequence, bool) {
if members := s.members(key, lang); len(members) > 0 { if members := s.members(key, lang); len(members) > 0 {
// key names a bundle: members are the bundles nested under it. // key names a bundle: members are the bundles nested under it.