Guide

Registry

A manifest of keys and lazy imports. Keys and aliases answer at once. Records load on the first lookup for their collection. How to register yours.

Six functions

ts
import {
  collectionKeys,
  hasCollection,
  getCollection,
  requireCollection,
  collections,
  registerCollection,
} from "@agntn/puzzles";

collectionKeys(); // ["80-bit", "bits", "ballet", …, "zden"], every key in the manifest
hasCollection("warpwallet"); // true, an alias for warp
await getCollection("bits"); // loads src/collections/bits and returns the instance
await getCollection(name); // undefined when name is nothing the manifest knows
await requireCollection("nope"); // throws UnknownCollectionError
await collections(); // every collection, loaded once, in manifest order

The first two are synchronous. They read the manifest and nothing else. The rest await an import(). The promise for a collection is shared, so two parallel callers load one module, not two.

A built-in key or alias comes back typed by the queries its collection takes. (await requireCollection("bits")).get(71) compiles, (await getCollection("gsmg")).get() too, and get(1) on warpwallet is a type error, not a silent miss. A plain string gets the loose AnyCollection, since nobody knows which collection it names until runtime.

The manifest

tssrc/collections/index.ts
export const builtins = [
  { key: "80-bit", load: () => import("./80-bit.ts").then((m) => m.eightyBit) },
  { key: "bits", load: () => import("./bits.ts").then((m) => m.bits) },
  // ...
] as const;

Only the key lives here. Importing @agntn/puzzles evaluates this list and the core. Not one record, not one collection class. The verification crypto doesn't: verify imports it on its first call. The CLI never imports the barrel, so puzzles --help stays light. A bundler sees a literal import() for each collection and splits it into its own chunk. Only ever ask about bits? The other collections stay unloaded. sideEffects in package.json names the CLI entry alone.

The cost is a new way to be wrong. A collection module that exists on disk and isn't in the list is invisible to the registry. A test compares the directory with the manifest, so it can't drift quietly. And @agntn/puzzles/collections/<key> serves the module directly either way.

Old names

peter_todd resolves to hash-collision and warpwallet to warp. That holds in hasCollection, getCollection and every identifier lookup. They're the names the earlier JSON dataset used. Scripts written against it still say them, and they should keep working.

b1000 is the newest old name. It resolves to bits, and since the puzzle numbers never moved, b1000/71 still finds its puzzle. What comes back answers bits/71 to id(), though. A script that stored the old identifiers and compares strings will notice.

Same deal for the collections that used to be snake_case. movie_enigma still finds movie-enigma, and hash_collision/sha1 finds hash-collision/sha1. Only the collection segment gets this. The puzzle names inside were renamed without aliases, so zden/level_1 is a miss now and zden/level-1 is the one to ask for. Aliases live in a Map, so constructor and __proto__ are unknown keys, not Object prototype properties.

Registering yours

ts
import { registerCollection } from "@agntn/puzzles";

registerCollection({ key: "mine", load: () => import("./mine").then((m) => m.mine) });
registerCollection(mine); // or an instance, registered as an already resolved entry

An entry with a loader stays lazy, the same as the built-ins. An instance registers as resolved. The same instance twice is a no-op. Another instance or entry under an existing key replaces what the key held, bits included. A fork with corrected data can stand in for the shipped one. Keep its query type, though: the types for bits still say get(71). Every aggregate view refreshes on the next read. all(), collectionSummaries(), dataVersion() and dataset() are memoized per registry snapshot, and a registration starts a new one. stats() and the tools recompute from those views on every call. The whole recipe is in Custom collections.

Where the collections come from

text
Collection<Query> (abstract)
├── NamedCollection        get("challenge-1") or get("warp/challenge-1")
│   ├── BalletCollection
│   ├── BitimageCollection
│   ├── CoinArtistCollection
│   ├── CoinGamesCollection
│   ├── DogesGambitCollection
│   ├── DugCollection
│   ├── HashCollisionCollection
│   ├── IAmABananaAmaaCollection
│   ├── LedgerDonjonCollection
│   ├── LuckyLurkerCollection
│   ├── MoveOverBrokersCollection
│   ├── RushwalletCollection
│   ├── SmithLyleMooreCollection
│   ├── WalkingBanksCollection
│   ├── WarpCollection
│   ├── WickexCollection
│   └── ZdenCollection
├── NumericCollection      get(71) or get("71") or get("bits/71")
│   ├── BitsCollection
│   ├── CryptoPuzzlesCollection
│   ├── GrycoinCollection
│   ├── MiniCollection
│   ├── QuizchainCollection
│   ├── Quizchain2Collection
│   ├── TeikhosCollection
│   └── WeaveCollection
└── SingletonCollection    get() or get("gsmg")
    ├── BitaddressCollection
    ├── BitapsCollection
    ├── BookQuizCollection
    ├── BraveNewWorldCollection
    ├── EightyBitCollection
    ├── GenesisCollection
    ├── GreatRiddleCollection
    ├── GsmgCollection
    ├── HuntingTimeCollection
    ├── LiberteGuidantLePeupleCollection
    ├── MineshopCollection
    ├── MovieEnigmaCollection
    ├── NatashaOtomoskiCollection
    ├── NineVw04Collection
    ├── PathToGreatnessCollection
    ├── PhyCollection
    ├── PicturePuzzleCollection
    ├── PowerfulMossCollection
    ├── ProofOfWritingCollection
    ├── RealBigBlockCollection
    ├── SatoshiBirthdayQuizCollection
    ├── SatoshiMazeCollection
    ├── SchoolOfBitcoinCollection
    ├── SeedPhraseCollection
    ├── TriviaBrainwalletCollection
    └── WealthInPoetryCollection

Each collection is a class with a static key, a static author built with party() and keyed for its author page, and the puzzle list. The three bases differ only in what get accepts. A singleton refuses to hold anything but the one puzzle whose id equals its key. It refuses at construction time, so a singleton id can't drift from the manifest.