Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273// evolve.mjs — breed a new pad from one you kept.//// generate.mjs rerolls from the contract every time: each pad is a fresh idea and// the loop never gets anywhere. Evolution is the other move — take something that// already earned attention and change ONE thing about it, so the good part is// carried and the new part is tested.//// A pad's genes are written in the way we already describe them: every one is a// VISUAL GENERATOR × a SYNTH VOICE (reaction-diffusion × bell, L-system fern ×// flute). That's the crossover point, and it means a mutation can be named out// loud instead of being a temperature knob://// voice — keep the eye, replace the ear// eye — keep the ear, replace the eye// cross — the winner's eye, another winner's ear// push — find what the TAP does and make the pad be about that// invert — the same idea, run backwards//// The parent's entire source goes in the prompt. Not a summary of it — a summary// is where the good part gets lost.//// node cancelok/evolve.mjs # breed one → disks/<name>.mjs// node cancelok/evolve.mjs --dry # print the prompt, write nothing
import { execFileSync } from "node:child_process";import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";import { resolve } from "node:path";import { REPO, crowd, pheromone, lineage, remember } from "./taste.mjs";
// launchd has never heard of ~/.local/bin, so the cron hands us an absolute// path. A bare "claude" works in a terminal and ENOENTs at 3am.const CLAUDE = process.env.CANCELOK_CLAUDE || "claude";
const DISKS = resolve(REPO, "system/public/aesthetic.computer/disks");const ENGINE = resolve(REPO, "system/public/aesthetic.computer/lib/pads.mjs");const dry = process.argv.includes("--dry");
// Fitness. Being kept is the floor; what we're really ranking on is whether you// COULDN'T LEAVE IT ALONE. Taps and revisits are worth more than an ok, because// an ok can be politeness and a revisit can't.const fitness = (p) => (p.ok - p.cancel) * 2 + Math.min(10, p.taps) + (p.revisits || 0) * 3 + Math.min(4, (p.dwellMs || 0) / 8000);
const MUTATIONS = [ { key: "voice", ask: `KEEP THE EYE, REPLACE THE EAR. The visual generator stays — same geometry,same allegory, same paint. Give it a completely different synth voice, and rewritethe score to suit that voice. The pad should look like its parent and sound like astranger. Make the visuals answer to the NEW sound (the visual must still BE thescore — if the eye no longer tracks the ear, you've failed).`, }, { key: "eye", ask: `KEEP THE EAR, REPLACE THE EYE. The score, the voice, the rhythm all stay —someone should be able to hum along to both. Throw the visual generator away andbuild a new one that is a DIFFERENT allegory for the same music. Same song, newinstrument to look at.`, }, { key: "push", ask: `FIND WHAT THE TAP DOES, AND MAKE THE PAD BE ABOUT THAT. The parent earnedits keep because someone reached out and touched it. Whatever onTap does, that isthe most interesting thing in the file — so promote it. Make the tap gesture theCENTER of the piece instead of a garnish: richer, more expressive under a drag,with more of the visual and the voice responding to where the finger is.`, }, { key: "invert", ask: `THE SAME IDEA, RUN BACKWARDS. Take the parent's core allegory and invertit — if it grows, make it decay; if it accumulates, make it erode; if it's a risingarpeggio, make it a falling one; if it's dense, make it sparse and let silence dothe work. Not a different idea: the SAME idea, seen from the other side.`, },];
const CROSS = { key: "cross", ask: (a, b) => `CROSSBREED. Take the VISUAL GENERATOR from ${a} and the SYNTH VOICEfrom ${b}. Not a collage of both files — a single coherent pad that inherits its eyefrom the first parent and its ear from the second, and makes them belong together.The visual must BE the score for the NEW voice.`,};
const source = (code) => readFileSync(resolve(DISKS, `${code}.mjs`), "utf8");const contract = readFileSync(ENGINE, "utf8").split("let cfg =")[0];const taken = () => readdirSync(DISKS).filter((f) => f.endsWith(".mjs")).map((f) => f.slice(0, -4));
const pads = (await crowd()).filter((p) => p.ok > 0 && fitness(p) > 0);if (!pads.length) { console.error("❌ nothing has been kept yet — there's nothing to breed from."); console.error(" run cancelok/generate.mjs until something earns an ok."); process.exit(2);}pads.sort((a, b) => fitness(b) - fitness(a));
// FAMILY QUOTA. Overnight the loop made five pendulums out of nine, because the// two pads anyone had kept were a pendulum and a plasma, and everything bred from// them. Fitness alone cannot see this: each child scored fine, and the line// converged anyway. So count the dynasties, and refuse to breed one that already// owns a third of the population.const kin = lineage();const root = (c) => { let at = c, hops = 0; while (kin[at]?.parent && hops++ < 50) at = kin[at].parent; return at;};const born = Object.keys(kin);const dynasty = {};for (const c of born) dynasty[root(c)] = (dynasty[root(c)] || 0) + 1;const SATURATED = Math.max(2, Math.ceil(born.length / 3));
const pool = pads.filter((p) => (dynasty[root(p.code)] || 0) < SATURATED);if (pool.length < pads.length) console.log( ` skipping ${pads.length - pool.length} saturated line(s) — a dynasty shouldn't own the deck`, );
// Every line saturated means the gene pool IS the problem, and breeding again —// from anyone — only deepens it. Refuse. Exit 3 tells the loop to go blind, which// is the only move that can introduce something the history doesn't already// contain. A breeder that never says "not from these" is just a copier.if (!pool.length) { console.error("❌ every line is saturated — breeding again would only deepen the rut."); process.exit(3);}
// Breed from the top few, not the top one. The best pad is a local maximum and// the loop's job is to leave it.const elite = pool.slice(0, Math.min(4, pool.length));const mum = elite[Math.floor(Math.random() * elite.length)];const canCross = elite.length > 1;
let mutation;if (canCross && Math.random() < 0.25) { const others = elite.filter((p) => p.code !== mum.code); const dad = others[Math.floor(Math.random() * others.length)]; mutation = { key: "cross", ask: CROSS.ask(mum.code, dad.code), dad: dad.code };} else { mutation = MUTATIONS[Math.floor(Math.random() * MUTATIONS.length)];}
const parentSrc = source(mum.code);const dadSrc = mutation.dad ? source(mutation.dad) : null;
const why = `${mum.code} earned this: ${mum.ok} ok / ${mum.cancel} cancel, ` + `played ${mum.taps}×, ${mum.revisits ? `came back to ${mum.revisits}×, ` : ""}` + `held on screen ${Math.round((mum.dwellMs || 0) / 1000)}s on average.`;
const prompt = `You are EVOLVING an existing pad for Aesthetic Computer.
A pad is a self-running audiovisual instrument piece. The whole screen is onebutton: tap or XY-drag to play. Every pad is a marriage of a VISUAL GENERATOR anda SYNTH VOICE, and the visual should BE the score — you should be able to SEE thesound you're hearing.
Here is the engine contract every pad must obey:
--- lib/pads.mjs (contract) ---${contract}--- end contract ---
THE PARENT — this pad was kept by real people. ${why}
--- disks/${mum.code}.mjs ---${parentSrc}--- end parent ---${ dadSrc ? `\nTHE SECOND PARENT — its voice is what you're borrowing.\n\n--- disks/${mutation.dad}.mjs ---\n${dadSrc}\n--- end second parent ---\n` : ""}${await pheromone()}
YOUR MUTATION — ${mutation.key.toUpperCase()}:
${mutation.ask}
This is a CHILD, not a cousin. The parent earned its keep, so inherit the partthat earned it and change only what the mutation asks you to change. A rewritethat shares nothing with its parent is not evolution, it's just another reroll —and we already have a tool for that.
Hard requirements:- A single .mjs file, a thin wrapper: import from "../lib/pads.mjs", define CONFIG with bpm/steps/hooks, call initPad(CONFIG) inside boot (NOT at import — the engine is a session singleton), export { boot, sim, paint, act }.- READ ctx.quality in onPaint and scale your cost by it. It must hold 60fps or the gate kills it before anyone sees it, and a killed child is a wasted generation.- Pick a name that is an invented syllable-word in the house style. NOT taken: ${taken().join(" ")}- Comment like the parent does: say WHY, name the allegory, no banner walls.
Output EXACTLY this and nothing else:
NAME: <the-name>TRAIT: <visual generator> x <synth voice>KIN: ${mum.code}${mutation.dad ? ` + ${mutation.dad}` : ""} (${mutation.key})\`\`\`javascript<the complete file>\`\`\``;
if (dry) { console.log(prompt); process.exit(0);}
console.log(`🧬 breeding ${mum.code} — mutation: ${mutation.key}${mutation.dad ? ` × ${mutation.dad}` : ""}`);// A language model, not an agent: `claude --print` can WRITE FILES, and a// generator that writes its own pad collides with itself on the name check.const raw = execFileSync(CLAUDE, ["--print", "--model", "sonnet", "--tools", ""], { input: prompt, encoding: "utf8", maxBuffer: 1024 * 1024 * 8, cwd: REPO,});
const name = /NAME:\s*([a-z][a-z0-9-]*)/i.exec(raw)?.[1]?.toLowerCase();// AC's font is ASCII — a stray × lands on screen as a `?`.const trait = (/TRAIT:\s*(.+)/.exec(raw)?.[1]?.trim() || "") .replace(/[×✕]/g, "x") .replace(/[^\x20-\x7e]/g, "");const code = /```(?:javascript|js)?\n([\s\S]*?)```/.exec(raw)?.[1];
if (!name || !code) { console.error("❌ couldn't parse a pad out of that.\n" + raw.slice(0, 400)); process.exit(1);}if (existsSync(resolve(DISKS, `${name}.mjs`))) { console.error(`❌ ${name} already exists — burned a generation on a collision.`); process.exit(1);}if (!/initPad\(/.test(code) || !/export\s*\{[^}]*boot/.test(code)) { console.error(`❌ ${name} doesn't honor the contract.`); process.exit(1);}
// A child that is its parent is not a child. This is the failure mode of breeding// and it's a quiet one — a near-copy passes the gate, earns the same ok, and// breeds again, and the line converges on one pad wearing different names. So:// count how much of the parent survived verbatim, and refuse a clone.const meat = (s) => s .split("\n") .map((l) => l.trim()) .filter((l) => l.length > 12 && !l.startsWith("//"));const kid = new Set(meat(code));const kept = meat(parentSrc).filter((l) => kid.has(l)).length;const echo = kept / Math.max(1, meat(parentSrc).length);if (echo > 0.85) { console.error( `❌ ${name} is ${Math.round(echo * 100)}% ${mum.code} — that's a copy, not a child.`, ); process.exit(1);}console.log(` inherited ${Math.round(echo * 100)}% of ${mum.code} verbatim`);
writeFileSync(resolve(DISKS, `${name}.mjs`), code);remember(name, { parent: mum.code, second: mutation.dad || null, mutation: mutation.key, trait, born: new Date().toISOString(),});console.log(`🐣 ${name} — ${trait} (${mum.code}${mutation.dad ? ` + ${mutation.dad}` : ""}, ${mutation.key})`);console.log(JSON.stringify({ code: name, name, trait, parent: mum.code, mutation: mutation.key }));