Guide

Registry

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

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

src/collections/index.ts
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.

@agntn/puzzles·MIT license· Public data about public puzzles. Balances come from the chains' explorers through the worker, cached for five minutes. Every key here was public before it landed in a record.