Writing algorithms
Author your own algorithm
An algorithm is the durable, reusable asset behind every theme: a named recipe
that maps a few anchor colors and knobs into a full, internally consistent token
register. It is a
What an algorithm is
The engine (@xoji/core) is mechanism: the open token register,
the dependency graph, the contrast math, the coverage contract, emit. It holds
no opinion about how a ramp should look. The algorithm is the
opinion: how this one derives. A theme is just one
materialized invocation of an algorithm, so the algorithm is the part worth
writing once and reusing.
The built-in set
Five algorithms ship blessed. They are ordinary mods, the same shape yours takes, and a good place to read taste off working examples.
| id | posture |
|---|---|
xoji-default | the neutral default; the shared taste with nothing turned up |
xoji-quiet | restrained; muted chroma, soft elevation |
xoji-loud | vibrant; boosted chroma and punchier depth |
xoji-hc | high contrast; an AAA floor for accessibility |
nxi-nite | Day/Night; a time-of-day pass layered on the base |
Anatomy of a mod
A mod is a directory: a manifest that declares it, and the code it runs. The
manifest names the mod, the color-math capability that unlocks the
cuti color primitives, and the script entry. The four exports the
host calls (graph, traced, manifest,
invariants) are registered for you by the authoring helper, so you
never touch the raw plumbing.
algorithms/sunrise/
├─ mod-manifest.json # declares the mod: name, capabilities, entry
└─ src/
├─ preset.ts # the taste, as pure data
└─ mod.ts # defineXojiAlgorithm(spec)
{
"$schema": "https://xript.dev/schema/mod-manifest/v0.7.json",
"xript": "0.7",
"name": "sunrise",
"version": "0.1.0",
"title": "Sunrise",
"family": "xoji",
"capabilities": ["color-math"],
"entry": { "script": "src/mod.js", "format": "script" }
}
Tier 1: a family preset
The fast path. defineXojiAlgorithm takes a taste vector and runs
the shared xoji derivation for everything you do not override.
xoji-default's entire definition is
{ id: "xoji-default" }; from there you turn knobs up or down.
// src/mod.ts
import { defineXojiAlgorithm } from "@xoji/core/authoring";
defineXojiAlgorithm({
id: "sunrise",
anchors: { bg: "#1a1410", accent: "#ff9e5e" },
vibrancy: 0.7,
chroma: { accent: 1.4, palette: 1.3 },
});
Every field is optional but id; the rest fall through to the shared defaults.
| field | type | what it sets |
|---|---|---|
id | string | the algorithm's id, how it loads and derives |
anchors | { bg, fg, accent } | baked default anchors; any omitted one derives from the others |
vibrancy | 0–1 | how saturated the derivation runs before the knob overrides it |
chroma | { accent, status, palette, neutral, accentTint } | per-family chroma multipliers over the shared derivation |
contrast | { floor, textOnFill } | the AA/AAA floors the derivation must clear |
elevation | { strength, alphaBoost } | shadow weight and overlay opacity |
knobs | string[] | the knobs this algorithm accepts (defaults to the shared set) |
passes | (preset, input) => Pass[] | ordered passes for a staged derivation (single-pass if omitted) |
Tier 2: from scratch
When you want to own the derivation, defineAlgorithm hands you the
input and the cuti color primitives and asks for the token graph
back: an array of nodes, each naming a token, its value, and the tokens it
derives from. Declare what you produce and how it
categorizes so the coverage contract and emitters know the shape.
import { defineAlgorithm } from "@xoji/core/authoring";
defineAlgorithm({
id: "mono",
produces: ["--bg-0", "--fg-0", "--accent", "--accent-fg"],
categories: { color: ["--bg-0", "--fg-0", "--accent", "--accent-fg"] },
knobs: ["scheme"],
derive(input, { cuti }) {
const bg = input.anchors?.bg ?? "#101216";
const accent = cuti.desaturate(input.anchors?.accent ?? "#8a8f98", 0.4);
const inks = ["#ffffff", "#0a0a0a"];
return [
{ name: "--bg-0", value: cuti.toHex(bg) },
{ name: "--fg-0", value: cuti.pickReadable(bg, inks), refs: ["--bg-0"] },
{ name: "--accent", value: cuti.toHex(accent) },
{ name: "--accent-fg", value: cuti.pickReadable(accent, inks), refs: ["--accent"] },
];
},
});
Deriving in passes
An algorithm that derives in stages lists ordered passes. Each is
a register → register transform; the first receives the anchors and knobs, and
later ones reshape what came before. A family algorithm typically starts from
settlePass (the shared derivation) and layers on top.
nxi-nite is the worked example, shifting the whole palette by
time of day after the base settles.
import { defineXojiAlgorithm, settlePass } from "@xoji/core/authoring";
defineXojiAlgorithm({
id: "day-cycle",
passes: (preset, input) => [
settlePass(preset, input), // the shared xoji derivation
timeOfDayShift(input), // a register → register transform
],
});
Prove it
The gauntlet runs your algorithm against extreme and random anchor sets and
checks every invariant it declares: contrast floors held, no token collapsed,
nothing parsed to NaN. Run it hosted to prove the sandboxed mod
that actually ships. The coverage check confirms a component's consumed tokens
are all in what you produce.
# prove the invariants across extremes + random anchor sets
npx xoji gauntlet -a sunrise --mode hosted --depth full
# confirm a component's consumed tokens are all produced
npx xoji coverage -a sunrise --consumed --bg-0,--fg-0,--accent
The gauntlet proves a theme is safe, not that it is good. For that,
derive a real theme and look at it under the components on
Load it
Build the mods to bundle your mod.ts into the self-contained
mod.js the host runs, and the algorithm loads by id, the same
path the built-ins take. From there it works everywhere the engine does: the
CLI, the importable API, and the browser.
npm run build:mods # bundles src/mod.ts → src/mod.js
npx xoji derive -a sunrise # loads by id, derives a theme
Questions or contributions live on