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:
Claude Opus 5
2026-07-30 03:03:06 +06:00
committed by bdeshi
parent d234c26fb7
commit 486bc7c3d3
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] comics/ # webcomics [spec]
the-long-monsoon/ the-long-monsoon/
_index.en.md # the series bundle; also the cascade point for it _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 index.en.md
page.png page.png
art/ # single images or sets [spec] 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 | | `nocache` | bool | Never cache this bundle's render. Named so absence means cacheable, per ADR-0002 |
| `summary` | string | Explicit summary; otherwise derived | | `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) | | `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 | | `cover` | string | Relative path to the lead image |
| `view` | string | Per-bundle View override (Arc 2) | | `view` | string | Per-bundle View override (Arc 2) |
| `styles` / `scripts` | []string | Page-specific assets, relative to the bundle | | `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. never live.
Emit `hreflang` and `canonical` from the variants that actually exist. 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 **Membership is structural** (ADR-0033): a bundle belongs to the series it is nested under, so
reduce to it (ADR-0016). Membership is `series`; position is whatever the type declares as its `order` `comics/the-long-monsoon/first-rain` is a chapter of `comics/the-long-monsoon`. The engine reads no
rule — date, sequence, or manual. Position lives in frontmatter, sparse, and never in the path, so `series` field. A bundle with bundles nested under it is a landing page and they are its members; a
inserting a chapter between two others is a single edit with no renames, no changed bundle keys and no landing page nested inside another series reports its own members rather than its siblings, since the
aliases. Resolution — first, prev, next, last, index, count — is defined once, honours `draft`, and deeper series is what the page is about. Membership therefore rides on the bundle key, which is permanent
respects the language fallback chain, so a missing Bengali chapter does not break Bengali prev/next. — 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 **Order is `order` ascending where set, then by name.** A member without `order` sorts after every member
graph. Pagination is a Query parameter plus a permalink rule for page 2+, still undecided. 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]` ## 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. 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 ## 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 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 `.../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 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 since nothing scopes terms, so `check` owes a near-duplicate report; and `tags` is now reserved at the top
level and inside every section. level and inside every section.
Revisit if: the pool becomes unusable in practice — and then the answer is curation, not namespacing. 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.