Registry
Six functions
import {
collectionKeys,
hasCollection,
getCollection,
requireCollection,
collections,
registerCollection,
} from "@agntn/puzzles";
collectionKeys(); // ["arweave", "b1000", "ballet", "bitaps", "bitimage", "gsmg", "hash_collision", "rushwallet", "warp", "zden"]
hasCollection("warpwallet"); // true, an alias for warp
await getCollection("b1000"); // loads src/collections/b1000 and returns the instance, or undefined
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.
The manifest
export const builtins: readonly CollectionEntry[] = [
{ key: "arweave", load: () => import("./arweave.ts").then((m) => m.arweave) },
{ key: "b1000", load: () => import("./b1000.ts").then((m) => m.b1000) },
// ...
];
Only the key lives here. Importing @agntn/puzzles evaluates this list and the core. Not one record, not one collection class. The verification crypto does come with the core, because verifyPuzzle is synchronous. The CLI never imports the barrel, so puzzles --help stays light. A bundler sees ten literal import() calls and splits each collection into its own chunk. Only ever ask about b1000? You never download the other nine. sideEffects in package.json names the CLI entry alone. The b1000 chunk is about 210 KB of records. The other nine together are 70.
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.
Two aliases
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. Aliases live in a Map, so constructor and __proto__ are unknown keys, not Object prototype properties.
Registering yours
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, b1000 included. A fork with corrected data can stand in for the shipped one. 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
Collection<Query> (abstract)
├── NamedCollection get("challenge_1") or get("warp/challenge_1")
│ ├── ArweaveCollection
│ ├── BalletCollection
│ ├── BitimageCollection
│ ├── HashCollisionCollection
│ ├── RushwalletCollection
│ ├── WarpCollection
│ └── ZdenCollection
├── NumericCollection get(71) or get("71") or get("b1000/71")
│ └── B1000Collection
└── SingletonCollection get() or get("gsmg")
├── BitapsCollection
└── GsmgCollection
Each collection is a class with a static key, a static author built with party(), 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 the two singleton ids can't drift from the manifest.
Puzzle records
One PuzzleSpec literal per puzzle handed to a factory for its chain. Builders for addresses and keys and transactions. Absent means absent and never null.
Lookups and views
get and requirePuzzle by identifier. selectPuzzles by collection and status. stats and the dataset snapshot with its deterministic data version.