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:
+1
-1
@@ -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 |
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user