record ADR-0033: series membership is structural

Answers the question that blocked sequences. A chapter belongs to the series it
is nested under, so the engine reads no `series` field; `order` stays, optional,
and a member without one sorts by name after every member carrying one.

This supersedes ADR-0016's membership clause and keeps its position rules — the
point of that ADR was that position never reaches a URL, which still holds. The
directory already states membership unambiguously; a second statement in
frontmatter can only agree or be a typo that silently orphans a chapter.

Cost, stated in the ADR: a series can never span directories, and membership now
rides on the bundle key, which ADR-0008 makes permanent.
This commit is contained in:
2026-08-01 02:23:34 +06:00
parent bfe613eae4
commit 50f3bd1942
2 changed files with 47 additions and 13 deletions
+28 -12
View File
@@ -31,7 +31,7 @@ overrides the defaults the binary embeds, so a bare root still renders.
comics/ # webcomics [spec]
the-long-monsoon/
_index.en.md # the series bundle; also the cascade point for it
first-rain/ # slug is the name, never the position (ADR-0016)
first-rain/ # a chapter because it is nested here (ADR-0033); slug is never the position
index.en.md
page.png
art/ # single images or sets [spec]
@@ -78,7 +78,7 @@ readable by templates (ADR-0002). Never add a required field.
| `nocache` | bool | Never cache this bundle's render. Named so absence means cacheable, per ADR-0002 |
| `summary` | string | Explicit summary; otherwise derived |
| `tags` | []string | Flat, case- and script-preserved as written. A scalar or a list. The URL form is lowercased with spaces hyphenated, so `Long Monsoon` and `long monsoon` are one term; scripts without case pass through unchanged (ADR-0018) |
| `series` / `order` | string / int | Series membership and position. Sparse by convention (10, 20, 30) so insertion is one edit; never appears in a URL (ADR-0016) |
| `order` | int | Position within the series this bundle is nested in (ADR-0033). Sparse by convention (10, 20, 30) so insertion is one edit; never appears in a URL (ADR-0016). Absent, the bundle orders by name, after every sibling carrying one |
| `cover` | string | Relative path to the lead image |
| `view` | string | Per-bundle View override (Arc 2) |
| `styles` / `scripts` | []string | Page-specific assets, relative to the bundle |
@@ -152,19 +152,35 @@ path — `/pages/about/` and `/bn/pages/about/`. `/en/…` permanently redirects
never live.
Emit `hreflang` and `canonical` from the variants that actually exist.
## Sequences, galleries, collections `[spec]`
## Sequences
All of these are a Query over bundle metadata with a stable sort; none justifies a new primitive.
Comics, serial fiction and multi-part essays all reduce to a sequence, which is why it is the one
grouping with a name (ADR-0016).
A **sequence** is the one that needs naming, because comics, serial fiction and multi-part essays all
reduce to it (ADR-0016). Membership is `series`; position is whatever the type declares as its `order`
rule — date, sequence, or manual. Position lives in frontmatter, sparse, and never in the path, so
inserting a chapter between two others is a single edit with no renames, no changed bundle keys and no
aliases. Resolution — first, prev, next, last, index, count — is defined once, honours `draft`, and
respects the language fallback chain, so a missing Bengali chapter does not break Bengali prev/next.
**Membership is structural** (ADR-0033): a bundle belongs to the series it is nested under, so
`comics/the-long-monsoon/first-rain` is a chapter of `comics/the-long-monsoon`. The engine reads no
`series` field. A bundle with bundles nested under it is a landing page and they are its members; a
landing page nested inside another series reports its own members rather than its siblings, since the
deeper series is what the page is about. Membership therefore rides on the bundle key, which is permanent
— re-parenting a chapter changes its URL and needs an alias like any other move.
A gallery is a Query plus an image View. Related posts are a Query with a scoring function, not a stored
graph. Pagination is a Query parameter plus a permalink rule for page 2+, still undecided.
**Order is `order` ascending where set, then by name.** A member without `order` sorts after every member
carrying one, the same way an undated bundle sorts after dated ones. Position lives in frontmatter, sparse
by convention, and never in the path, so inserting a chapter between two others is a single edit with no
renames, no changed keys and no aliases.
Resolution — first, prev, next, last, index, count — is defined once, and members resolve through the
language fallback chain, so a chapter missing in Bengali still appears in Bengali reading order rather
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.
## Galleries, collections `[spec]`
Both are a Query over bundle metadata with a stable sort; neither justifies a new primitive. A gallery is
a Query plus an image View. Related posts are a Query with a scoring function, not a stored graph.
## Taxonomies `[spec]`
+19 -1
View File
@@ -161,7 +161,7 @@ boundary must route through the normaliser rather than accepting a raw string.
Revisit if: never for normalisation. Slug derivation rules change with the default locale.
## ADR-0016 — Sequence position is metadata and never appears in a URL
Date: 2026-07-28 · Status: accepted
Date: 2026-07-28 · Status: accepted; its membership clause is superseded by ADR-0033 (position rules stand)
Decision: a bundle's slug is its name, never its position — `comics/the-long-monsoon/the-flood/`, not
`.../02-the-flood/`. Position comes from a single declared source per type (`order` in the type
declaration: date, sequence, or manual), sparse by convention (10, 20, 30) so inserting between two
@@ -384,3 +384,21 @@ caller is what earned `Query.Tag` rather than a guess. Expensive — tag hygiene
since nothing scopes terms, so `check` owes a near-duplicate report; and `tags` is now reserved at the top
level and inside every section.
Revisit if: the pool becomes unusable in practice — and then the answer is curation, not namespacing.
## ADR-0033 — Series membership is structural; `order` is optional
Date: 2026-07-30 · Status: accepted (supersedes ADR-0016's membership clause, not its position rules)
Decision: a bundle is a member of a series because it is nested under the series' landing bundle —
`comics/the-long-monsoon/first-rain` under `comics/the-long-monsoon` — so the engine reads no `series`
field. Reading order is `order` ascending where it is set, then by key; a member without `order` sorts
after every member that has one. A bundle with bundles nested under it is a series landing page, and one
nested inside another series reports its own members rather than its siblings.
Why: the directory already states membership unambiguously, and a second statement of it in frontmatter
can only agree or be a bug — a typo in `series` silently orphans a chapter, which is precisely the class
of error a single source of truth removes. Making `order` optional keeps the cheap case cheap: a series
whose filenames already sort correctly needs no frontmatter at all, and `order` is for when they do not.
Consequence: cheap — nothing to declare, nothing to keep in sync, and moving a series moves its
membership with it. Expensive — a series can never span directories, so a cross-directory collection
needs a Query over some other field, not this; and membership now depends on the bundle key, which
ADR-0008 makes permanent, so re-parenting a chapter is a permalink event with an alias.
Revisit if: a real collection must span sections. Then it is a declared taxonomy (ADR-0032's second
half), not a change to how a series is joined.