List Files
Every deck, collection, and wanted list is one markdown file: decks/<name>.md, collections/<name>.md, or wanted/<name>.md. Ritual reads and writes these files, and you can edit them by hand. This page describes what goes in them.
The rule is lenient in, canonical out. The reader accepts several spellings of a card line. Every write (an editor save, a sync, a card command, or cleanup) emits one canonical form, so files converge on it over time.
The three list types
Section titled “The three list types”| Deck | Collection | Wanted list | |
|---|---|---|---|
| Copies | A quantity on the line (- 4 Lightning Bolt) |
One line per copy | One line per copy |
Printing (SET:CN) |
Optional | Required | Optional |
[finish] |
Yes | Yes | Yes |
[condition] |
Yes | Yes | Never |
[lang] |
Yes | Yes | Yes |
[labels] |
proxy, no-proxy |
sale, trade, keep, proxy, and no-… |
Never |
#tags (card tags) |
Yes | Yes | Yes |
{note}, &N |
Yes | Yes | Yes |
| Sections with a fixed role | Commander, Companion, Oathbreaker, Sideboard… | None — every ## Section is a free-form group |
None |
| Front-matter keys | format, tags, labels, description, image, sync stamps |
labels, description, image |
description, image |
A minimal example of each:
---format: commandertags: [budget]---
# Winota Stax
## Commander
- 1 Winota, Joiner of Forces (IKO:216) &1
## Main
- 1 Sol Ring (LTC:284) &2- 4 Lightning Bolt (LEA:161) [foil] &3---labels: [trade]---
# Trade Binder
- Black Lotus (LEA:232) [LP] [keep] {first edition} &1- Mana Crypt (2XM:270) [foil] [ja] &2- Mana Crypt (2XM:270) [foil] [ja] &3# Wants
- Counterspell &1- Sol Ring (LEA:270) [foil] &2Front matter
Section titled “Front matter”An optional YAML block between --- fences at the top of the file. Each type has its own keys. Any key not listed here survives every save: a deck’s block is re-dumped, and a collection’s or wanted list’s block is kept byte for byte.
| Key | Types | Meaning |
|---|---|---|
format |
deck | The deck format key (commander, modern, …). Missing: inferred from the sections and written on next save. |
tags |
deck | Free-text tags describing the list (aggro, budget), not its cards. Edited with metadata; shown on the site. |
labels |
deck, collection | The list’s default card labels, inherited by every line and adjusted by its own [labels] token. Positive only; never on a wanted list. |
description |
all | A prose blurb the built site prints above the cards. |
image |
all | The list’s cover image: a {card: N}, {file: …} or {url: …} mapping. |
sourceId, sourceUrl |
deck | The deck’s identity on its sync source. |
lastSynced, sourceUpdatedAt |
deck | Stamped by deck-sync; never hand-edited. |
A deck with no format and a ## Commander section is inferred as Commander.
Older files may carry name: or created:. Neither is a key today; the list’s name is its # Title heading. A deck save strips both, and cleanup moves an old name: into the H1.
Default labels and descriptions
Section titled “Default labels and descriptions”A deck or collection can declare default labels in its front matter. Every list type can carry a description:
---description: Everything I will trade away.labels: [sale, trade]---
# Trade BinderEvery entry inherits the default, adjusted by its own [labels] token if it has one (see Card labels). A default is positive only: no-sale belongs on a card line, never in labels:. The values a list can carry:
- Collection:
saleandtrade(together or alone), orkeeporproxy(each alone). - Deck:
proxyalone. Every deck line then counts as a proxy, which marks a whole playtest deck at once; a real card in it carries[no-proxy].
---format: commanderlabels: [proxy]---An empty list, or no key, means no default. On a deck, a value it cannot carry (labels: [sale, proxy]) is dropped whole, not filtered down to the allowed part. It is also a parse warning, like a refused card-line token: the next whole-file save would delete the key, so the warning names it and whole-file writes are blocked until you fix it. On a collection, an unreadable labels: value is ignored with an advisory, and the block is kept as written.
Set the default with:
ritual metadata set <list> labels …, which writes only the front matter;- the
editsession’s🏷️ Edit List Labelsaction, applied on the session’s next Save (which rewrites the whole file in canonical form); - the admin editor’s Labels button;
- the MCP
set_list_metadatatool; - a hand edit.
How the block survives edits:
- Card-line saves keep the block byte for byte, including unknown hand-authored keys. A block whose YAML cannot be read is carried verbatim with an advisory.
- A metadata edit re-dumps the YAML.
ritual metadata, theeditsession’s🏷️ Edit List Labelsaction, the admin Labels button, andset_list_metadataall rewrite the block. Every key and value survives; comments and quoting style do not. - The
editsession’s action refuses to run when the block’s YAML cannot be read, since it cannot merge over keys it cannot see. Fix the block by hand. Every other session edit still carries it verbatim.
description: is written the same way (ritual metadata, the admin/HTTP route, or set_list_metadata) and is the one key every list type carries. A wanted list’s own keys are description: and the cover image:, which set-list-image writes; any other block on one is preserved.
A cover written from outside while an edit session is open is dropped by that session’s next save, because the session re-emits the block it read when it opened. See the known gap.
Title and sections
Section titled “Title and sections”The first # Title line outside any fenced code block names the list, on all three types. The sites and pickers display it, and new and rename write it. Commands address a list by its file name (see List Names), which cleanup keeps equal to the title. A file with no H1 is named after its file name.
## Section headings are the only other structural marker. Cards before the first heading belong to an implicit Main section. Collections and wanted lists use sections as free groupings. In a deck, a section’s role comes from an exact match (case-insensitive, trimmed) against this table:
| Role | Section names | Behavior |
|---|---|---|
commander |
Commander, Commanders, Command Zone |
The command zone; excluded from the main-deck count, exported as the Commander board. |
companion |
Companion |
Exported as the Companion board. |
oathbreaker |
Oathbreaker, Signature Spell |
The Oathbreaker command zone. |
sideboard |
Sideboard |
Exported as the Sideboard board; excluded from the main-deck count. |
maybeboard |
Maybeboard |
Counts toward no total; left out of every decklist export and the site’s .txt download. |
tokens |
Tokens, Token |
Same as maybeboard. |
main |
Main, Mainboard, Deck — and every other name |
Part of the main deck, exported to the Deck board. |
Matching is exact, never by substring or word: ## Token Generators, ## Commander Damage Notes, and ## Sideboard (post-board) are ordinary main-deck sections.
Empty sections are handled by role:
- An empty
## Maybeboard/## Tokensheading is deleted on purpose by a whole-file rewrite and reported asDropped empty section. - Any other empty heading (
## Sideboard,## Commander,## Creatures, …), in a deck that has cards elsewhere, is kept and written back bare, without a warning. Removing or moving a section’s last card leaves its heading the same way.
Card lines
Section titled “Card lines”Canonical form
Section titled “Canonical form”Every writer emits this, in this order, with one space between tokens and defaults omitted:
- [qty] Name (SET:CN) [finish] [cond] [lang] [labels] #tag, tag {note} &N| Token | Spelling | Notes |
|---|---|---|
- |
the bullet | Written on every line of every type, so the file renders as a list anywhere markdown is rendered. |
qty |
an integer, decks only on write | Decks always write one (- 1 Sol Ring). Collections and wanted lists never do: one line per copy. |
Name |
the card name, trimmed | Free text. Tokens are peeled off the right-hand end, so a parenthesized word that is not a SET:CN pair (Very Cryptic Command (Untap)) stays in the name. |
(SET:CN) |
(LEA:161) |
Set code uppercase in the file, lowercase in memory. Collector number verbatim (★, †, letters allowed). Always a pair; a set alone is not a printing. |
[finish] |
[foil], [etched] |
nonfoil is the default and is not written. |
[cond] |
[LP], [MP], [HP], [DMG] |
NM is the default and is not written. |
[lang] |
[ja], [zhs], … |
A lowercase Scryfall language code. English is the default and is not written. |
[labels] |
[sale], [trade,no-sale], [no-proxy] |
The line’s label override, adjusting the list default. Positives first, then no- negatives. keep and proxy each stand alone. |
#tags |
#Ramp, #Card Draw, Ramp, #Binder: Trade |
The card’s tags: one #, then the tags comma-separated. Written deduplicated and sorted. |
{note} |
{any text} |
A free-text note. Runs to the last } on the line, so a note may contain braces. An empty {} is dropped. |
&N |
&12 |
The persistent card ID, always last. |
A line with a (SET:CN) is pinned to that printing. A line without one is name-only and stands for any printing of the card.
A bracket token at the end of the line must be one the grammar knows. [Alpha] is Unrecognized token [Alpha] and refuses the line.
Which tokens each type accepts
Section titled “Which tokens each type accepts”| Token | Deck | Collection | Wanted |
|---|---|---|---|
| quantity | yes | read only* | read only* |
| printing | yes | required | yes |
| finish | yes | yes | yes |
| condition | yes | yes | no |
| language | yes | yes | yes |
| labels | proxy, no-proxy |
all four, and no-… |
no |
| tags | yes | yes | yes |
| note | yes | yes | yes |
| id | yes | yes | yes |
* Accepted when reading and expanded on save — see Read tolerances.
A no- negation is accepted wherever its label is: [no-proxy] on a deck, any of the four on a collection.
The parser refuses a line, naming the offending token and its column, when it finds:
- an unrecognized bracket token;
- a token written twice (tags excepted: a line may carry several tag tokens, and a repeated tag folds away);
- a malformed tag token: a
#with nothing after it, or a tag holding a forbidden character such as#R&D; - a known token stuck inside the name, or written without whitespace around it;
- a well-formed token the type does not carry. The message says so plainly:
[NM] is not a wanted list token — wanted lists never carry a condition.
A collection line with no (SET:CN) is refused too (missing-printing), since a stored copy is a specific physical card.
One exception: a deck line carrying a label a deck cannot hold ([keep], [sale,trade], [no-sale]) keeps the card and drops the labels, with a warning.
A refused line is reported as a warning naming the file and line. A whole-file rewrite would delete it, so it blocks every whole-file write of that file until fixed: editor saves, syncs, cleanup’s rewrite, and move with a deck on either side. Line-preserving commands (add-card, remove-card, set-card, note) leave such lines untouched.
Read tolerances
Section titled “Read tolerances”The reader accepts all of the following. The next save rewrites them into the canonical form.
- Any bracket-token order.
[NM] [foil]and[foil] [NM]mean the same thing. Tokens are recognized by value, not position. - Any run of whitespace between tokens and around the name.
- The bullet is optional on a deck line. A deck line is recognized by its leading quantity (
4 Lightning Bolt,- 4 Lightning Bolt). A collection or wanted-list line is recognized by its bullet, so a bulletless line there is prose, not a card. Nxquantities.4x Lightning Boltreads as four. A run of four or more digits with noxis part of the name (1996 World Champion).- A quantity on a collection or wanted-list line.
- 4 Lightning Bolt (LEA:161)reads as four copies and is expanded into four lines on the next save. This is an advisory, not a warning, since nothing is lost. The first copy keeps the line’s&N; the others get fresh ids. - The Arena / MTGO export form
Name (SET) CN, with a trailing*F*/*E*finish marker, and Moxfield’s bulk-edit formName (SET) *F* CN. Both become(SET:CN)plus[foil]/[etched]. A parenthesized set with no collector number stays part of the name (Very Cryptic Command (Untap)is a real card) and raises an advisory. - Repeated tags, or one
#per tag.#ramp #staple #rampreads as the tagsrampandstapleand is written back as one token,#ramp, staple. Case is kept:Rampandrampare two tags. //comment lines are skipped on read and dropped on write._in set codes (PLST_X).
Card IDs (&N)
Section titled “Card IDs (&N)”Every card line ends in a persistent numeric id. Ids are sequential from 1 within each file. An id is stable across every edit that keeps the line. It is released to a reuse pool only when the line is removed outright; decrementing a deck quantity keeps it. New lines take the smallest free id.
Never hand-author or renumber them. Commands that write card lines, or that rely on every line having an id, fill in missing ids before they run and save them to the file; see the card-ID backfill. The ids are an internal handle for change tracking, the admin editors, custom art, and cover images. No UI shows them.
Labels, tags, and categories
Section titled “Labels, tags, and categories”Ritual has three ways to say something about a card. They look alike, since each is a short word attached to a card, but they are different kinds of thing, and each answers a different question:
- A label tells Ritual what to do with a copy: sell it, trade it, keep it, or price it as a proxy.
[sale],[proxy]. - A tag describes the physical copy: something true of that card whichever list it sits in.
#Signed,#Trade Binder. - A category describes the card’s role in one list: why it is in this deck.
Ramp,Board Wipes.
| Label | Tag | Category | |
|---|---|---|---|
| Belongs to | one card line (&N) |
one card line: the copy | a card name in one list, covering every line of that name |
| Vocabulary | closed: sale, trade, keep, proxy |
open: any text | open: any text, with suggested defaults |
| List types | collections (all four), decks (proxy/no-proxy only) |
all three | all three |
| Several per card? | sale + trade, plus any no- negations |
yes, unordered | yes, ordered: the first is the card’s primary category |
| Moved to another list | kept as far as the destination type allows | always kept | never kept: a role belongs to the list |
| Stored | […] token on the card line |
#a, b token on the card line |
<name>.categories.json beside the list |
| Changes Ritual’s behavior | yes: pricing, sell/trade filters | no | no |
| Other sites’ name | — | — | Archidekt categories, Moxfield tags |
Some consequences:
- Moxfield’s “tags” are Ritual’s categories, not Ritual’s tags. A Moxfield URL import brings them in as categories, and
export --dialect moxfieldwrites categories out as Moxfield tags. Ritual’s own tags have no counterpart on either site. - Archidekt’s categories are Ritual’s categories, except the ones that are boards. Archidekt uses categories for
Commander,Sideboard, andMaybeboardtoo; Ritual reads those as sections. See Boards and categories.deck-sync --sync-categorieskeeps the rest in step. - A
Keeptag is not the[keep]label, and aRamptag is not theRampcategory. The names are free text; only the kind decides what Ritual does with them. - A deck’s front-matter
tags:key is about the deck, not its cards (aggro,budget); see Front matter.
Which one to use: if Ritual should act on it, it is a label. If it would still be true after you moved the card to another list, it is a tag. If it only makes sense inside this list, it is a category.
Card labels
Section titled “Card labels”A card entry can carry labels, a bracket token on its line ([sale,trade], [keep], [no-proxy]) declaring what you intend to do with that copy. Each list type carries a different set:
| List type | Labels it carries |
|---|---|
| Collection | sale, trade, keep, proxy |
| Deck | proxy only (and no-proxy) |
| Wanted list | none |
sale(“For sale”) andtrade(“For trade”) are the only two that combine, as[sale,trade].keep(“To keep”) andproxy(“Proxy”) are each exclusive. Neither combines with any other label.[sale,keep]or[keep,proxy]is a parse warning, as is a label the list’s type does not carry. The entry is kept and its labels dropped, and the warning blocks whole-file rewrites until it is fixed.proxymarks a copy that is not a real card, which is why it is the one label a deck carries. It has pricing consequences.
A card line adjusts the list default
Section titled “A card line adjusts the list default”A list can declare a default in its front matter (labels:); see Default labels and descriptions. A card’s token adjusts that default rather than replacing it:
- a label written plainly (
trade) adds it; - a label written as
no-<label>(no-sale, “Not for sale”) takes an inherited one away; - an added label also displaces any inherited label it cannot stand beside, so
[keep]under asaledefault is justkeep.
A card’s effective labels are its positives, plus the default, minus the labels it negates:
| Default | Card token | Effective labels |
|---|---|---|
[sale] |
none | sale |
[sale] |
[trade] |
sale, trade |
[sale] |
[trade,no-sale] |
trade |
[sale,trade] |
[no-sale,no-trade] |
none (“No labels”) |
[sale,trade] |
[keep] |
keep |
[proxy] |
[no-proxy] |
none: a real card |
| none | [sale] |
sale |
On the line, positives come first, then negatives, each in the order sale, trade, keep, proxy: [trade,no-sale]. A label both added and negated ([proxy,no-proxy]) is a parse warning like an exclusive pair. A negation the line’s own positive already makes pointless ([keep,no-sale]) is dropped when the line is written. A negation of a label the default does not hold ([no-proxy] in a deck without a default) is kept: it keeps the card’s meaning if the default changes later.
Set an override with:
set-card --label(--label trade,no-sale);--label noneclears it;add-card --labelfor a new card (--label no-proxyadds a real card to a proxy-default deck);- the
editsession’s🏷️ Change Labelaction; - the web editors’ Set Label… menu item (“Use list default” clears it).
The pickers offer the state you want the card in (For sale, For trade, For sale + trade, To keep, Proxy, No labels, or Use list default; on a deck, Proxy, Not a proxy, or Use list default), and write the smallest token that gives it under the list’s default. “No labels” and “Not a proxy” appear only when the list has a default to take away. Every picker offers only what its list type carries, and asking for sale or no-sale on a deck is a usage error naming the supported labels.
The site badges a card’s own token: in the list view no-proxy shows as Not a proxy and no-sale as Not for sale; on a card tile, where the name line is short, the negation is the label struck through (PROXY), with that wording as its tooltip. The card’s details show its effective labels.
set-card --label is also how you repair a line whose token the parser refuses. It replaces the token outright, so it is the one edit the refusal does not block. Every other edit to that line refuses rather than drop the token silently, including a remove-card that would decrement the line’s quantity.
Labels are part of a deck line’s identity for merging. Copies added by add-card, the editors, or ritual move join an existing line only when its effective labels match theirs. A proxy never merges into the line holding the real copies, and a real card never inherits [proxy] from a line it lands beside.
Proxies carry no price
Section titled “Proxies carry no price”A card whose effective labels include proxy is not a real card, so Ritual prices it at zero everywhere rather than looking a price up:
pricereports it at0with the unpriced reasonproxy, shows PROXY in its price cell instead ofN/A, and counts it as a card but not as unpriced.- The generated site records
0in every currency, leaves proxies out of list totals and missing-price counts, and never asks a buyer for a quote on one. selldrops proxy entries before matching, so they are never quoted, counted, or merged into an identical real copy.
Custom art follows the same rule. A card with both reports the unpriced reason custom-art and shows CUSTOM; custom art wins.
Card tags
Section titled “Card tags”A card entry on any list type can carry tags: your own words for the card as a copy (Signed, Trade Binder, Gift from Dad). Tags follow the card wherever it moves. A card’s role within one list (what Archidekt calls a category and Moxfield a tag) is a separate, per-list thing; see Labels, tags, and categories. On the line, tags are one # token after the labels and before the note, comma-separated:
- 1 Sol Ring (LTC:284) [proxy] #Ramp, Staple &2- Mox Ruby #Budget, Reserved List {any copy} &3A label is an instruction to Ritual from a closed list ([proxy] changes pricing); a tag means whatever you meant. A Keep tag has no connection to the [keep] label. Tags drive grouping, sorting and filtering on the generated site and select cards for export --tags.
A tag is plain text. Spaces are fine (Card Draw) and case is kept (Ramp and ramp are two tags). A tag cannot contain #, ,, &, brackets, braces or parentheses. A line’s tags are written deduplicated and sorted. The # is file punctuation only; the editors, the site and the changelog never show it.
A deck’s front-matter tags: key describes the deck (ritual metadata set <deck> tags edh,budget, or the edit session’s 🔖 Edit Deck Tags row). It never applies to a card. Only the #tags token on a card line holds card tags.
Edit a card’s tags with set-card --tag / --untag, add-card --tag, or the editors’ tag dialogs (ritual edit, the admin editors). Every change is recorded as one changelog event per tag that actually changed (Added tag "Ramp" to "Sol Ring" &2, Removed tag "Staple" from "Sol Ring" &2), never as a whole-set replacement. An add and a remove of the same tag on the same card cancel out, so re-adding a tag you removed earlier in a session leaves no changelog trace.
Like labels, tags are part of a deck line’s identity for merging. Copies added with different tags land on their own line.
Card language
Section titled “Card language”Every card entry has a language, written as a lowercase bracket token after the finish and condition and before labels and the note.
- Mana Crypt (2XM:270) [foil] [ja] [sale,trade] &3- 3 Counterspell (LEA:55) [de] &12- Sol Ring (C21:263) [zhs] &4The vocabulary is Scryfall’s language codes (en es fr de it pt ja ko ru zhs zht he la grc ar sa ph), not ISO codes: Chinese is zhs/zht. The token is omitted for English. A bare line always means en, whatever the configured default, so a list file stays self-describing.
Adding a card never prompts for a language. New cards get the configured defaultLanguage; an edit session can override it for its own adds. Change one copy afterwards with set-card --language or an editor’s language action.
Under a non-English default, the printing pickers mark printings that do not exist in that language. Picking one records the language that does exist (English when available) rather than a language Scryfall has no card object for. Availability is checked against the card cache, which holds every language when defaultLanguage is non-English, with a direct Scryfall lookup when the cache cannot answer.
Wanted-list card states
Section titled “Wanted-list card states”Each card on a wanted list is in one of three states, which decides how it is priced:
| State | Format | Pricing Behavior |
|---|---|---|
| Name only | - Card Name |
Uses cheapest printing across all sets |
| Printing | - Card Name (SET:CN) |
Uses cheapest finish of that exact printing |
| Fully specified | - Card Name (SET:CN) [finish] |
Uses the exact printing and finish specified |
Fenced code blocks
Section titled “Fenced code blocks”A list file may carry a fenced code block: an example line, a template, a snippet of output. Everything inside a fence is prose. A card-looking line inside a fence is not a card, a ## Heading is not a section, an &N is not a card ID, and none of it is reported as unreadable.
# My Binder
## Main
- Sol Ring (C19:221) &1
Cards are written like this:
```- Card Name (SET:CN) [finish] [condition] {note} &N- Black Lotus (LEA:232) &99```
- Lightning Bolt (LEA:161) &2That file holds two cards. The - Black Lotus (LEA:232) &99 line is not counted, priced, exported, offered by a picker, or targeted by add-card, set-card, remove-card, note, or move. &99 is not in use, so a future card may take that ID. The &N backfill leaves fenced lines unstamped, and every line-preserving edit keeps the block byte for byte.
Fence rules:
- Three or more backticks or three or more tildes, indented by up to three spaces, with an optional info string (
```markdown). - The closing fence uses the same character, is at least as long, and carries nothing after it.
- Fences do not nest. Tildes inside a backtick fence are ordinary content, and vice versa.
- An unclosed fence runs to the end of the file (the CommonMark rule), so a stray
```hides every card line below it. If cards go missing from a list, check for an unbalanced fence.
Inline code spans (`like this`) and four-space indented blocks are not code. A four-space indent looks like a nested list item, so an indented block’s card lines are read as real cards and its ``` delimiters as unreadable lines. Use a fenced block whenever a list file needs prose card lines.
Whole-file rewrites
Section titled “Whole-file rewrites”Surfaces that rewrite a whole file from its parsed cards cannot re-emit a fenced block, so they treat one like an unreadable line:
| Surface | Behavior with a fenced block |
|---|---|
| The admin editors’ save (and the MCP tools that reuse it) | Refuses with a 400 and writes nothing |
cleanup |
Reports the block and skips the content rewrite; a drifted file name is still corrected |
deck-sync / collection-sync |
Held back by the unreadable-lines gate (-y/--yes accepts the loss) |
import --append |
Refuses and writes nothing |
A deck on either side of move |
Refuses and writes nothing |
ritual edit sessions |
Warns on load and drops the block on the next save — check the session output before saving |
The one-shot card commands (add-card, set-card, remove-card, note) preserve lines and work normally, as does a move between two collections or wanted lists. The exception is an append into an unclosed fence: a card line appended after it would be prose, so add-card and move refuse rather than write a line no later parse can see.
Categories (<name>.categories.json)
Section titled “Categories (<name>.categories.json)”A category is a card’s role in one list: Ramp, Removal, Board Wipes. Archidekt calls it a category and Moxfield a tag. For how it differs from a label and a tag, see Labels, tags, and categories.
Edit categories with set-card --categories/--no-categories, the ritual categories subcommands (list/rename/order/remove), or the editors’ 🗂 Edit Categories action and the list menu’s Rename Category… / Reorder Categories… rows (ritual edit).
Categories are never written on a card line. They live in a JSON file beside the list (a “sidecar”):
{ "order": ["Ramp", "Draw", "Removal", "Artifacts"], "cards": { "Rhystic Study": ["Draw"], "Sol Ring": ["Ramp", "Artifacts"] }}- Keyed by card name. One assignment covers every line of that name in the list, whatever its printing, section or quantity. Lookups fold case and whitespace. The stored key is the name as the card line spells it, including the
A // Bspelling of a double-faced card. cardsis ordered per card; the first entry is the primary category. Reordering is a real edit.orderis the display order of the list’s vocabulary. Categories a card uses butorderdoes not name are appended when Ritual next writes the file: thedefaultCategoriesconfig vocabulary first, in its configured order, then the rest alphabetically.- A category name is any non-empty plain text without a comma or a control character. It is looser than the tag rule because a category is never written on a card line, so names like Archidekt’s
Tokens & Extrassurvive a sync unchanged. Case is kept;Rampandrampare one category with two spellings. - Stale names are kept, with a warning. A
cardskey naming a card the list no longer holds loads with a warning. It is pruned by the list’s own save (an editor session, an admin save), by a cross-listmovethat rewrites the list (only when the move could read every card line in it), and byritual cleanup.ritual categoriesreports stale entries and never prunes them. - A malformed sidecar is refused as a whole. It is never partially loaded and never silently overwritten. A list with an unreadable sidecar still saves.
- Empty means gone. A sidecar with no vocabulary and no cards is deleted rather than written as
{}. - It has its own
.sha256. Unlike<name>.art.json, this sidecar is part of the list’s recorded history.detect-changesdetects hand edits and records them in the list’s.changes.mdasSet categories of "Sol Ring" to Ramp, Artifacts,Set category order to …, orRenamed category "Draw" to "Card Draw". A sidecar Ritual did not itself last write keeps its stale hash, so a hand edit is never silently declared recorded.
The sites read this file. The built site includes it in each list’s detail JSON and offers the Category groupings, sort and filter. The admin and public editors write it through their Edit Categories… and Manage categories dialogs. ritual export’s categories/primaryCategory columns read it, and a CSV import’s categories column writes it.
The .changes.md changelog
Section titled “The .changes.md changelog”Every list has an append-only <name>.changes.md file recording its card changes. Each entry is a ## <ISO timestamp> heading, one prose - line per change, then a fenced ritual-changes block holding the same changes as JSON Lines in the same order:
# Changelog for Winota Stax
## 2026-03-07T22:01:21.452Z
- Added "Demonic Tutor" (UMA:75) [foil] &3- Removed "Misty Rainforest" &4
```ritual-changes{"action":"add","cardName":"Demonic Tutor","cardId":3,"set":"uma","collectorNumber":"75","finish":"foil"}{"action":"remove","cardName":"Misty Rainforest","cardId":4}```- The block is authoritative. Ritual reads only the
ritual-changesblock. The prose lines are for people (and translated for display on the sites) and are never parsed. - Hand-written text is preserved. Prose you add after an entry’s block travels with that entry through the
historyeditor and is written back verbatim. - It is a data format. The prose is always English whatever the UI locale, and the file is git-diffable. Edit it with
historyor the admin Change History page rather than by hand. - Legacy entries written before the block existed are never converted. They keep their prose, yield zero events, and are named by an advisory.
What cleanup normalizes
Section titled “What cleanup normalizes”ritual cleanup is the migration for everything above. One pass rewrites every list in canonical form:
- bullets on deck lines, canonical token order and spacing, uppercase set codes, defaults omitted;
- flat-list quantities expanded to one line per copy;
- a legacy
name:turned into the# TitleH1, withname:andcreated:dropped (tags:and every other key kept); &Non every line;- a card spelled with a repeated face (
Steam Vents // Steam Vents, Scryfall’s name for a reversible printing) renamed to the card’s own name, with its category assignments moved along; - each file renamed after its title;
- stale names pruned from the list’s
.categories.json, which is re-serialized canonically.
--dry-run previews all of it without writing. cleanup never touches a .changes.md. It is idempotent: a second run writes nothing.
Ritual is unofficial Fan Content permitted under the Fan Content Policy. Not approved or endorsed by Wizards of the Coast. Portions of the materials used are property of Wizards of the Coast. ©Wizards of the Coast LLC.
Card data, images and symbols come from Scryfall, which is not affiliated with this project. Ritual is free software under the AGPL-3.0. License and attribution.