diff --git a/docs/README.md b/docs/README.md index b424276..b989463 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | | `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 | | `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 | diff --git a/docs/content-model.md b/docs/content-model.md index 878f5a6..adccb84 100644 --- a/docs/content-model.md +++ b/docs/content-model.md @@ -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 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 -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 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 "prev" is the earlier entry. -`draft` is not honoured yet, because no bundle carries it and nothing else excludes drafts either; the -feature that adds the field adds it here in the same change. +A draft or future-dated member is not in the sequence at all: membership resolves through the same +`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 -`khosra demo ` 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 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 diff --git a/docs/decisions.md b/docs/decisions.md index bb62d7f..f5f5765 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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. ## 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 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. diff --git a/docs/state.md b/docs/state.md index 4627136..f4e8c35 100644 --- a/docs/state.md +++ b/docs/state.md @@ -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 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 -`content-model.md`'s table — including `draft` and `type` — lands in `Extra` unread, so that table +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 `type` — lands in `Extra` unread, so that table 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 @@ -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 | | 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 `` 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 | ## Open questions diff --git a/docs/surface.md b/docs/surface.md index 19f49ec..96c47c0 100644 --- a/docs/surface.md +++ b/docs/surface.md @@ -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: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: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:268 func (s *Site) keys() []string - site.go:284 type Sequence struct -- site.go:304 func (s *Site) Sequence(key, lang string) (Sequence, bool) -- site.go:325 func (s *Site) parent(key string) (string, bool) -- site.go:335 func (s *Site) has(key string) bool -- site.go:344 func (s *Site) members(series, lang string) []Bundle -- site.go:369 func (b Bundle) hasTag(slug string) bool -- site.go:382 func (b Bundle) Section() string -- site.go:394 func (s *Site) Everything() []Bundle -- site.go:407 func (s *Site) Sections() []string +- site.go:305 func (s *Site) Sequence(key, lang string) (Sequence, bool) +- site.go:326 func (s *Site) parent(key string) (string, bool) +- site.go:336 func (s *Site) has(key string) bool +- site.go:345 func (s *Site) members(series, lang string) []Bundle +- site.go:370 func (b Bundle) hasTag(slug string) bool +- site.go:383 func (b Bundle) Section() string +- site.go:395 func (s *Site) Everything() []Bundle +- site.go:408 func (s *Site) Sections() []string ## internal/ext/check — 231 lines + 168 test diff --git a/internal/content/site.go b/internal/content/site.go index 5bdd500..eee2f1a 100644 --- a/internal/content/site.go +++ b/internal/content/site.go @@ -299,8 +299,9 @@ type Sequence struct { // page is about. // // 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 -// because no bundle carries it yet (content-model.md). +// its place in the reading order rather than breaking prev/next (ADR-0009). Membership goes through Lookup, +// 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) { if members := s.members(key, lang); len(members) > 0 { // key names a bundle: members are the bundles nested under it.